@felan-ai/felan 0.21.16 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/dependencies.js +1 -1
- package/dist/dependencies.js.map +1 -1
- package/dist/extensions.d.ts +1 -1
- package/dist/extensions.d.ts.map +1 -1
- package/dist/extensions.js +2 -2
- package/dist/extensions.js.map +1 -1
- package/dist/herdr.d.ts +22 -0
- package/dist/herdr.d.ts.map +1 -0
- package/dist/herdr.js +208 -0
- package/dist/herdr.js.map +1 -0
- package/dist/insights.d.ts +1 -1
- package/dist/insights.d.ts.map +1 -1
- package/dist/insights.js +11 -8
- package/dist/insights.js.map +1 -1
- package/dist/memory/control.d.ts +1 -1
- package/dist/memory/control.d.ts.map +1 -1
- package/dist/memory/control.js +90 -34
- package/dist/memory/control.js.map +1 -1
- package/dist/memory/coordinator.d.ts +13 -2
- package/dist/memory/coordinator.d.ts.map +1 -1
- package/dist/memory/coordinator.js +416 -67
- package/dist/memory/coordinator.js.map +1 -1
- package/dist/memory/dreamer.d.ts +7 -1
- package/dist/memory/dreamer.d.ts.map +1 -1
- package/dist/memory/dreamer.js +145 -59
- package/dist/memory/dreamer.js.map +1 -1
- package/dist/memory/history-view.d.ts +26 -0
- package/dist/memory/history-view.d.ts.map +1 -0
- package/dist/memory/history-view.js +192 -0
- package/dist/memory/history-view.js.map +1 -0
- package/dist/memory/history.d.ts +33 -0
- package/dist/memory/history.d.ts.map +1 -0
- package/dist/memory/history.js +314 -0
- package/dist/memory/history.js.map +1 -0
- package/dist/memory/run-retention.d.ts +12 -0
- package/dist/memory/run-retention.d.ts.map +1 -0
- package/dist/memory/run-retention.js +294 -0
- package/dist/memory/run-retention.js.map +1 -0
- package/dist/memory/run.d.ts +67 -0
- package/dist/memory/run.d.ts.map +1 -0
- package/dist/memory/run.js +417 -0
- package/dist/memory/run.js.map +1 -0
- package/dist/memory/store.d.ts +67 -3
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +517 -110
- package/dist/memory/store.js.map +1 -1
- package/dist/resume.d.ts.map +1 -1
- package/dist/resume.js +12 -7
- package/dist/resume.js.map +1 -1
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +27 -18
- package/dist/runtime.js.map +1 -1
- package/dist/session-picker.d.ts +8 -1
- package/dist/session-picker.d.ts.map +1 -1
- package/dist/session-picker.js +7 -4
- package/dist/session-picker.js.map +1 -1
- package/dist/settings.d.ts +0 -3
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +0 -22
- package/dist/settings.js.map +1 -1
- package/dist/subagents/agent-transcript.d.ts +4 -1
- package/dist/subagents/agent-transcript.d.ts.map +1 -1
- package/dist/subagents/agent-transcript.js +16 -4
- package/dist/subagents/agent-transcript.js.map +1 -1
- package/package.json +24 -23
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { mkdir, rm } from 'node:fs/promises';
|
|
3
|
-
import { createMemorySnapshot, createMemoryProjectionSnapshot, createSessionCheckpoint, extractSourceIds, hydrateMemoryDirectory, readMemoryDirectory,
|
|
3
|
+
import { createMemorySnapshot, createMemoryProjectionSnapshot, createSessionCheckpoint, extractSourceIds, hydrateMemoryDirectory, readMemoryDirectory, } from '@felan-ai/ext-memory';
|
|
4
4
|
import { ModelRuntime, SessionManager, } from '@felan-ai/agent-core';
|
|
5
5
|
import { acquireLocalMemoryLease, } from './lease.js';
|
|
6
6
|
import { resolveLocalMemoryProject } from './project.js';
|
|
7
|
-
import { createDefaultLocalMemoryDreamRunner, materializeMemoryInput, } from './dreamer.js';
|
|
8
|
-
import { LocalMemoryStore, } from './store.js';
|
|
7
|
+
import { createDefaultLocalMemoryDreamRunner, MemoryModelUnavailableError, materializeMemoryInput, } from './dreamer.js';
|
|
8
|
+
import { LocalMemoryStore, LocalMemoryControlError, } from './store.js';
|
|
9
|
+
import { LocalMemoryRun, sanitizeMemoryDiagnostic } from './run.js';
|
|
9
10
|
const MEMORY_INPUT_BLOCKED_MESSAGE = 'Some memory checkpoints could not be materialized; evidence remains pending';
|
|
11
|
+
const MEMORY_AUTOMATIC_UPDATE_THRESHOLD = 5;
|
|
12
|
+
const MEMORY_AUTOMATIC_INTERVAL_MS = 60 * 60 * 1_000;
|
|
10
13
|
export class LocalMemoryCoordinator {
|
|
11
14
|
#options;
|
|
12
15
|
#contexts = new Map();
|
|
@@ -16,11 +19,16 @@ export class LocalMemoryCoordinator {
|
|
|
16
19
|
#enabled;
|
|
17
20
|
#selectedModel;
|
|
18
21
|
#disposed = false;
|
|
22
|
+
#monitor;
|
|
23
|
+
#monitoring;
|
|
24
|
+
#enableVersion = 0;
|
|
19
25
|
constructor(options) {
|
|
20
26
|
this.#options = options;
|
|
21
27
|
this.#enabled = options.enabled !== false;
|
|
22
28
|
this.#selectedModel = options.selectedModel;
|
|
23
29
|
this.#dreamRunner = options.dreamRunner ?? createDefaultLocalMemoryDreamRunner();
|
|
30
|
+
if (this.#enabled)
|
|
31
|
+
this.#startMonitor();
|
|
24
32
|
}
|
|
25
33
|
createSessionHost(options) {
|
|
26
34
|
return new LocalMemorySessionHost(this, options);
|
|
@@ -36,19 +44,39 @@ export class LocalMemoryCoordinator {
|
|
|
36
44
|
}
|
|
37
45
|
setSelectedModel(model) {
|
|
38
46
|
this.#selectedModel = model;
|
|
47
|
+
for (const context of this.#contexts.values()) {
|
|
48
|
+
context.blocked.clear();
|
|
49
|
+
if (context.initialized)
|
|
50
|
+
void this.#trackScheduleIfPending(context);
|
|
51
|
+
}
|
|
39
52
|
}
|
|
40
53
|
setEnabled(enabled) {
|
|
54
|
+
this.#enableVersion += 1;
|
|
41
55
|
this.#enabled = enabled;
|
|
56
|
+
if (enabled)
|
|
57
|
+
this.#startMonitor();
|
|
58
|
+
else if (this.#monitor) {
|
|
59
|
+
clearInterval(this.#monitor);
|
|
60
|
+
this.#monitor = undefined;
|
|
61
|
+
}
|
|
42
62
|
for (const context of this.#contexts.values()) {
|
|
43
|
-
if (!enabled
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
if (!enabled)
|
|
64
|
+
this.#cancelScheduledRun(context);
|
|
65
|
+
if (!enabled)
|
|
66
|
+
this.#cancelContext(context, 'Memory processing was disabled', true);
|
|
67
|
+
if (enabled) {
|
|
68
|
+
context.storageBlocked = false;
|
|
69
|
+
context.writerBlocked = false;
|
|
70
|
+
context.blocked.clear();
|
|
71
|
+
if (context.initialized)
|
|
72
|
+
this.#startRecovery(context);
|
|
46
73
|
}
|
|
47
74
|
context.state = enabled ? 'idle' : 'disabled';
|
|
48
75
|
}
|
|
49
76
|
if (enabled) {
|
|
50
77
|
for (const context of this.#contexts.values()) {
|
|
51
|
-
|
|
78
|
+
if (context.initialized)
|
|
79
|
+
void this.#trackScheduleIfPending(context);
|
|
52
80
|
}
|
|
53
81
|
}
|
|
54
82
|
this.#emitStatusChange();
|
|
@@ -57,26 +85,36 @@ export class LocalMemoryCoordinator {
|
|
|
57
85
|
try {
|
|
58
86
|
const context = await this.#context(cwd);
|
|
59
87
|
const state = await context.store.status();
|
|
88
|
+
const control = await context.store.readControl();
|
|
60
89
|
const processed = Object.values(state.processed);
|
|
61
90
|
const lastProcessedAt = processed
|
|
62
91
|
.map((entry) => entry.processedAt)
|
|
63
92
|
.sort()
|
|
64
93
|
.at(-1);
|
|
65
94
|
return {
|
|
66
|
-
enabled: this.#enabled,
|
|
67
|
-
state: this.#enabled
|
|
95
|
+
enabled: this.#enabled && !control.autoDisabled,
|
|
96
|
+
state: !this.#enabled || control.autoDisabled ? 'disabled'
|
|
97
|
+
: control.nextRetryAt && Date.parse(control.nextRetryAt) > this.#now() ? 'scheduled'
|
|
98
|
+
: context.state === 'error' ? 'error'
|
|
99
|
+
: control.activeAttempt ? 'processing' : context.state,
|
|
68
100
|
pendingCheckpoints: Object.keys(state.pending).length,
|
|
69
101
|
memoryFingerprint: state.memoryFingerprint,
|
|
70
102
|
...(lastProcessedAt === undefined ? {} : { lastProcessedAt }),
|
|
71
103
|
...(context.message === undefined ? {} : { message: context.message }),
|
|
104
|
+
consecutiveFailures: control.consecutiveFailures,
|
|
105
|
+
...(control.nextRetryAt ? { nextRetryAt: control.nextRetryAt } : {}),
|
|
106
|
+
...(control.autoDisabled ? { autoDisabled: control.autoDisabled } : {}),
|
|
107
|
+
...(control.activeAttempt || control.lastOutcome ? {
|
|
108
|
+
lastRunId: control.activeAttempt?.runId ?? control.lastOutcome.runId,
|
|
109
|
+
} : {}),
|
|
72
110
|
};
|
|
73
111
|
}
|
|
74
|
-
catch {
|
|
112
|
+
catch (error) {
|
|
75
113
|
return {
|
|
76
114
|
enabled: this.#enabled,
|
|
77
115
|
state: this.#enabled ? 'error' : 'disabled',
|
|
78
116
|
pendingCheckpoints: 0,
|
|
79
|
-
message: 'Local memory storage is unavailable',
|
|
117
|
+
message: error instanceof LocalMemoryControlError ? error.message : 'Local memory storage is unavailable',
|
|
80
118
|
};
|
|
81
119
|
}
|
|
82
120
|
}
|
|
@@ -95,9 +133,8 @@ export class LocalMemoryCoordinator {
|
|
|
95
133
|
async recordCheckpoint(cwd, checkpoint) {
|
|
96
134
|
const context = await this.#context(cwd);
|
|
97
135
|
if (await context.store.recordCheckpoint(checkpoint)) {
|
|
98
|
-
context.checkpointVersion += 1;
|
|
99
136
|
context.blocked.delete(checkpoint.sessionId);
|
|
100
|
-
await this.#
|
|
137
|
+
await this.#trackScheduleIfPending(context);
|
|
101
138
|
this.#emitStatusChange();
|
|
102
139
|
}
|
|
103
140
|
}
|
|
@@ -114,8 +151,7 @@ export class LocalMemoryCoordinator {
|
|
|
114
151
|
this.#cancelScheduledRun(context);
|
|
115
152
|
}
|
|
116
153
|
context.blocked.clear();
|
|
117
|
-
const
|
|
118
|
-
const run = this.#run(context);
|
|
154
|
+
const run = this.#run(context, true);
|
|
119
155
|
context.run = run;
|
|
120
156
|
try {
|
|
121
157
|
await run;
|
|
@@ -123,10 +159,31 @@ export class LocalMemoryCoordinator {
|
|
|
123
159
|
finally {
|
|
124
160
|
if (context.run === run)
|
|
125
161
|
context.run = undefined;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
162
|
+
await this.#trackScheduleIfPending(context);
|
|
163
|
+
}
|
|
164
|
+
return this.status(cwd);
|
|
165
|
+
}
|
|
166
|
+
async retryProject(cwd) {
|
|
167
|
+
if (!this.#enabled)
|
|
168
|
+
return this.status(cwd);
|
|
169
|
+
const context = await this.#context(cwd);
|
|
170
|
+
if (!this.#enabled || this.#disposed)
|
|
171
|
+
return this.status(cwd);
|
|
172
|
+
const enableVersion = this.#enableVersion;
|
|
173
|
+
this.#cancelScheduledRun(context);
|
|
174
|
+
try {
|
|
175
|
+
await context.store.resetProjectControl(this.#now(), () => this.#enabled && !this.#disposed && this.#enableVersion === enableVersion);
|
|
129
176
|
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
context.abort?.abort(new Error('Memory processing was explicitly retried'));
|
|
181
|
+
if (context.run)
|
|
182
|
+
await context.run;
|
|
183
|
+
context.blocked.clear();
|
|
184
|
+
context.storageBlocked = false;
|
|
185
|
+
context.message = undefined;
|
|
186
|
+
await this.runNow(cwd);
|
|
130
187
|
return this.status(cwd);
|
|
131
188
|
}
|
|
132
189
|
async canonicalDirectory(cwd) {
|
|
@@ -137,17 +194,22 @@ export class LocalMemoryCoordinator {
|
|
|
137
194
|
if (this.#disposed)
|
|
138
195
|
return;
|
|
139
196
|
this.#disposed = true;
|
|
197
|
+
if (this.#monitor)
|
|
198
|
+
clearInterval(this.#monitor);
|
|
199
|
+
this.#monitor = undefined;
|
|
140
200
|
for (const context of this.#contexts.values()) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
context.timer = undefined;
|
|
144
|
-
context.abort?.abort();
|
|
201
|
+
this.#cancelScheduledRun(context);
|
|
202
|
+
this.#cancelContext(context, 'Memory processing was cancelled during shutdown', false);
|
|
145
203
|
}
|
|
146
204
|
while (true) {
|
|
147
205
|
const work = [...this.#contexts.values()].flatMap((context) => [
|
|
148
206
|
...(context.run ? [context.run] : []),
|
|
207
|
+
...(context.ready ? [context.ready] : []),
|
|
208
|
+
...(context.recovery ? [context.recovery] : []),
|
|
149
209
|
...context.followUps,
|
|
150
210
|
]);
|
|
211
|
+
if (this.#monitoring)
|
|
212
|
+
work.push(this.#monitoring);
|
|
151
213
|
if (work.length === 0)
|
|
152
214
|
break;
|
|
153
215
|
await Promise.allSettled(work);
|
|
@@ -158,67 +220,250 @@ export class LocalMemoryCoordinator {
|
|
|
158
220
|
if (this.#disposed)
|
|
159
221
|
throw new Error('Local memory coordinator is closed');
|
|
160
222
|
const project = await resolveLocalMemoryProject(cwd);
|
|
223
|
+
if (this.#disposed)
|
|
224
|
+
throw new Error('Local memory coordinator is closed');
|
|
161
225
|
const existing = this.#contexts.get(project.key);
|
|
162
|
-
if (existing)
|
|
226
|
+
if (existing) {
|
|
227
|
+
await this.#initialize(existing);
|
|
163
228
|
return existing;
|
|
164
|
-
|
|
229
|
+
}
|
|
230
|
+
const store = new LocalMemoryStore(this.#options.agentDir, project, {
|
|
231
|
+
...(this.#options.retryDelaysMs ? { retryDelaysMs: this.#options.retryDelaysMs } : {}),
|
|
232
|
+
});
|
|
165
233
|
const context = {
|
|
166
234
|
project,
|
|
167
235
|
cwd,
|
|
168
236
|
...(this.#options.sessionDir === undefined ? {} : { sessionDir: this.#options.sessionDir }),
|
|
169
237
|
store,
|
|
170
|
-
ready:
|
|
171
|
-
|
|
238
|
+
ready: undefined,
|
|
239
|
+
initialized: false,
|
|
240
|
+
recovery: undefined,
|
|
241
|
+
recoveryCompleted: false,
|
|
242
|
+
diagnosticsReconciled: false,
|
|
172
243
|
timer: undefined,
|
|
244
|
+
timerDueAt: undefined,
|
|
173
245
|
run: undefined,
|
|
174
246
|
followUps: new Set(),
|
|
175
247
|
abort: undefined,
|
|
248
|
+
attempt: undefined,
|
|
249
|
+
storageBlocked: false,
|
|
250
|
+
writerBlocked: false,
|
|
251
|
+
statusSignature: undefined,
|
|
176
252
|
blocked: new Map(),
|
|
177
|
-
checkpointVersion: 0,
|
|
178
253
|
state: this.#enabled ? 'idle' : 'disabled',
|
|
179
254
|
message: undefined,
|
|
180
255
|
};
|
|
181
256
|
this.#contexts.set(project.key, context);
|
|
182
|
-
await context
|
|
257
|
+
await this.#initialize(context);
|
|
258
|
+
return context;
|
|
259
|
+
}
|
|
260
|
+
async #initialize(context) {
|
|
261
|
+
if (context.ready) {
|
|
262
|
+
await context.ready;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (context.initialized && context.diagnosticsReconciled) {
|
|
266
|
+
this.#startRecovery(context);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const ready = this.#initializeContext(context).finally(() => {
|
|
270
|
+
if (context.ready === ready)
|
|
271
|
+
context.ready = undefined;
|
|
272
|
+
});
|
|
273
|
+
context.ready = ready;
|
|
274
|
+
await ready;
|
|
275
|
+
}
|
|
276
|
+
async #initializeContext(context) {
|
|
277
|
+
if (!context.initialized) {
|
|
278
|
+
await context.store.initialize(this.#now()).then(() => {
|
|
279
|
+
context.initialized = true;
|
|
280
|
+
context.state = this.#enabled ? 'idle' : 'disabled';
|
|
281
|
+
context.message = undefined;
|
|
282
|
+
}).catch(async (error) => {
|
|
283
|
+
if (!(error instanceof LocalMemoryControlError))
|
|
284
|
+
throw error;
|
|
285
|
+
const current = await context.store.readCurrent();
|
|
286
|
+
if (current.fingerprint !== (await context.store.status()).memoryFingerprint)
|
|
287
|
+
throw error;
|
|
288
|
+
context.message = error.message;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
if (!context.initialized)
|
|
292
|
+
return;
|
|
293
|
+
if (!context.diagnosticsReconciled) {
|
|
294
|
+
try {
|
|
295
|
+
const lease = await acquireLocalMemoryLease(context.store.projectDirectory, this.#options.leaseOptions);
|
|
296
|
+
if (lease) {
|
|
297
|
+
try {
|
|
298
|
+
await this.#reconcileRunDiagnostics(context, lease);
|
|
299
|
+
context.diagnosticsReconciled = true;
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
await lease.release();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
context.diagnosticsReconciled = true;
|
|
308
|
+
context.storageBlocked = true;
|
|
309
|
+
context.message = `Memory diagnostics recovery is blocked: ${sanitizeMemoryDiagnostic(error)}`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (this.#disposed)
|
|
313
|
+
return;
|
|
183
314
|
if (this.#enabled)
|
|
184
315
|
void this.#trackScheduleIfPending(context);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
316
|
+
this.#startRecovery(context);
|
|
317
|
+
}
|
|
318
|
+
#startRecovery(context) {
|
|
319
|
+
if (this.#disposed || !this.#enabled || this.#options.recover === false
|
|
320
|
+
|| context.recoveryCompleted || context.recovery)
|
|
321
|
+
return;
|
|
322
|
+
const recovery = this.#recover(context).then((completed) => {
|
|
323
|
+
if (completed)
|
|
324
|
+
context.recoveryCompleted = true;
|
|
325
|
+
}).catch(() => {
|
|
326
|
+
context.message = 'Memory startup recovery could not complete';
|
|
327
|
+
}).finally(() => {
|
|
328
|
+
if (context.recovery === recovery)
|
|
329
|
+
context.recovery = undefined;
|
|
330
|
+
});
|
|
331
|
+
context.recovery = recovery;
|
|
332
|
+
}
|
|
333
|
+
#now() { return this.#options.now?.() ?? Date.now(); }
|
|
334
|
+
#startMonitor() {
|
|
335
|
+
if (this.#disposed || this.#monitor)
|
|
336
|
+
return;
|
|
337
|
+
const interval = this.#options.monitorIntervalMs ?? 2_000;
|
|
338
|
+
if (!Number.isFinite(interval) || interval < 1)
|
|
339
|
+
throw new Error('Memory monitor interval must be positive');
|
|
340
|
+
this.#monitor = setInterval(() => {
|
|
341
|
+
if (this.#monitoring)
|
|
342
|
+
return;
|
|
343
|
+
this.#monitoring = this.#refresh().catch(() => { }).finally(() => { this.#monitoring = undefined; });
|
|
344
|
+
}, interval);
|
|
345
|
+
this.#monitor.unref?.();
|
|
346
|
+
}
|
|
347
|
+
async #refresh() {
|
|
348
|
+
if (this.#disposed)
|
|
349
|
+
return;
|
|
350
|
+
for (const context of this.#contexts.values()) {
|
|
351
|
+
try {
|
|
352
|
+
await this.#initialize(context);
|
|
353
|
+
const observedAttempt = context.attempt;
|
|
354
|
+
const control = await context.store.readControl();
|
|
355
|
+
if (observedAttempt && context.attempt === observedAttempt && (!this.#enabled || control.autoDisabled
|
|
356
|
+
|| control.activeAttempt?.runId !== observedAttempt.runId
|
|
357
|
+
|| control.activeAttempt?.generation !== observedAttempt.generation)) {
|
|
358
|
+
this.#cancelContext(context, 'Memory processing was cancelled by another session', true);
|
|
359
|
+
}
|
|
360
|
+
context.writerBlocked = false;
|
|
361
|
+
if (context.initialized)
|
|
362
|
+
await this.#trackScheduleIfPending(context);
|
|
363
|
+
const signature = JSON.stringify([this.#enabled, context.state, context.message, control.updatedAt]);
|
|
364
|
+
if (signature !== context.statusSignature) {
|
|
365
|
+
context.statusSignature = signature;
|
|
366
|
+
this.#emitStatusChange();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
context.state = 'error';
|
|
371
|
+
context.message = sanitizeMemoryDiagnostic(error);
|
|
372
|
+
context.abort?.abort(error);
|
|
373
|
+
this.#emitStatusChange();
|
|
374
|
+
}
|
|
190
375
|
}
|
|
191
|
-
return context;
|
|
192
376
|
}
|
|
193
|
-
|
|
194
|
-
|
|
377
|
+
#cancelContext(context, reason, cancelPersisted) {
|
|
378
|
+
const alreadyAborted = context.abort?.signal.aborted ?? false;
|
|
379
|
+
context.abort?.abort(new Error(reason));
|
|
380
|
+
if (alreadyAborted || !context.initialized || (!cancelPersisted && !context.attempt))
|
|
381
|
+
return;
|
|
382
|
+
const cancellation = cancelPersisted
|
|
383
|
+
? context.store.cancelActiveAttempt(this.#now())
|
|
384
|
+
: context.store.finishAttempt(undefined, context.attempt, { status: 'cancelled', reason }, this.#now());
|
|
385
|
+
const followUp = cancellation
|
|
386
|
+
.then(() => { })
|
|
387
|
+
.catch(() => { })
|
|
388
|
+
.finally(() => { context.followUps.delete(followUp); });
|
|
389
|
+
context.followUps.add(followUp);
|
|
390
|
+
}
|
|
391
|
+
async #schedule(context, dueAt) {
|
|
392
|
+
if (this.#disposed || !this.#enabled || context.run || context.storageBlocked)
|
|
393
|
+
return;
|
|
394
|
+
if (context.timer && context.timerDueAt === dueAt)
|
|
195
395
|
return;
|
|
396
|
+
this.#cancelScheduledRun(context);
|
|
196
397
|
context.state = 'scheduled';
|
|
398
|
+
const delay = Math.max(0, dueAt - this.#now());
|
|
197
399
|
const timer = setTimeout(() => {
|
|
198
400
|
if (context.timer !== timer)
|
|
199
401
|
return;
|
|
200
402
|
context.timer = undefined;
|
|
201
|
-
|
|
403
|
+
context.timerDueAt = undefined;
|
|
404
|
+
const run = this.#run(context).catch((error) => {
|
|
405
|
+
context.state = 'error';
|
|
406
|
+
context.storageBlocked = true;
|
|
407
|
+
context.message = sanitizeMemoryDiagnostic(error);
|
|
408
|
+
this.#emitStatusChange();
|
|
409
|
+
}).finally(async () => {
|
|
202
410
|
context.run = undefined;
|
|
203
411
|
await this.#trackScheduleIfPending(context);
|
|
204
412
|
});
|
|
205
413
|
context.run = run;
|
|
206
414
|
}, delay);
|
|
207
415
|
context.timer = timer;
|
|
416
|
+
context.timerDueAt = dueAt;
|
|
208
417
|
timer.unref?.();
|
|
209
418
|
}
|
|
210
|
-
async #scheduleIfPending(context
|
|
419
|
+
async #scheduleIfPending(context) {
|
|
211
420
|
if (!this.#enabled || this.#disposed)
|
|
212
421
|
return;
|
|
213
422
|
try {
|
|
423
|
+
const control = await context.store.readControl();
|
|
424
|
+
if (control.autoDisabled) {
|
|
425
|
+
this.#cancelScheduledRun(context);
|
|
426
|
+
context.state = 'disabled';
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (context.storageBlocked)
|
|
430
|
+
return;
|
|
431
|
+
if (context.writerBlocked)
|
|
432
|
+
return;
|
|
214
433
|
const state = await context.store.status();
|
|
215
434
|
const pending = Object.values(state.pending);
|
|
216
|
-
if (
|
|
217
|
-
|
|
435
|
+
if (control.activeAttempt) {
|
|
436
|
+
this.#cancelScheduledRun(context);
|
|
437
|
+
}
|
|
438
|
+
else if (pending.some(({ checkpoint }) => !isBlockedCheckpoint(context, checkpoint))) {
|
|
439
|
+
const acceptedUpdates = await context.store.pendingAcceptedUpdates((checkpoint) => !isBlockedCheckpoint(context, checkpoint));
|
|
440
|
+
if (acceptedUpdates < MEMORY_AUTOMATIC_UPDATE_THRESHOLD) {
|
|
441
|
+
this.#cancelScheduledRun(context);
|
|
442
|
+
if (context.state === 'scheduled')
|
|
443
|
+
context.state = 'idle';
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const processedAt = Object.values(state.processed)
|
|
447
|
+
.map(({ processedAt }) => Date.parse(processedAt))
|
|
448
|
+
.filter(Number.isFinite)
|
|
449
|
+
.reduce((latest, at) => Math.max(latest, at), 0);
|
|
450
|
+
const dueAt = control.nextRetryAt
|
|
451
|
+
? Date.parse(control.nextRetryAt)
|
|
452
|
+
: processedAt === 0
|
|
453
|
+
? this.#now()
|
|
454
|
+
: processedAt + (this.#options.debounceMs ?? MEMORY_AUTOMATIC_INTERVAL_MS);
|
|
455
|
+
await this.#schedule(context, dueAt);
|
|
218
456
|
}
|
|
219
457
|
else if (pending.length > 0) {
|
|
458
|
+
this.#cancelScheduledRun(context);
|
|
459
|
+
const message = context.state === 'blocked' ? context.message : undefined;
|
|
220
460
|
context.state = 'blocked';
|
|
221
|
-
context.message = MEMORY_INPUT_BLOCKED_MESSAGE;
|
|
461
|
+
context.message = message ?? MEMORY_INPUT_BLOCKED_MESSAGE;
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
this.#cancelScheduledRun(context);
|
|
465
|
+
if (context.state === 'scheduled')
|
|
466
|
+
context.state = 'idle';
|
|
222
467
|
}
|
|
223
468
|
}
|
|
224
469
|
catch {
|
|
@@ -226,15 +471,15 @@ export class LocalMemoryCoordinator {
|
|
|
226
471
|
context.message = 'Local memory storage is unavailable';
|
|
227
472
|
}
|
|
228
473
|
}
|
|
229
|
-
#trackScheduleIfPending(context
|
|
230
|
-
const followUp = this.#scheduleIfPending(context
|
|
474
|
+
#trackScheduleIfPending(context) {
|
|
475
|
+
const followUp = this.#scheduleIfPending(context).finally(() => {
|
|
231
476
|
context.followUps.delete(followUp);
|
|
232
477
|
});
|
|
233
478
|
context.followUps.add(followUp);
|
|
234
479
|
return followUp;
|
|
235
480
|
}
|
|
236
|
-
async #run(context) {
|
|
237
|
-
if (this.#disposed || !this.#enabled)
|
|
481
|
+
async #run(context, bypassRetryAt = false) {
|
|
482
|
+
if (this.#disposed || !this.#enabled || context.storageBlocked)
|
|
238
483
|
return;
|
|
239
484
|
context.state = 'processing';
|
|
240
485
|
context.message = undefined;
|
|
@@ -243,15 +488,18 @@ export class LocalMemoryCoordinator {
|
|
|
243
488
|
lease = await acquireLocalMemoryLease(context.store.projectDirectory, this.#options.leaseOptions);
|
|
244
489
|
}
|
|
245
490
|
catch (error) {
|
|
491
|
+
context.writerBlocked = true;
|
|
246
492
|
context.state = 'error';
|
|
247
493
|
context.message = safeProcessingMessage(error);
|
|
248
494
|
return;
|
|
249
495
|
}
|
|
250
496
|
if (!lease) {
|
|
497
|
+
context.writerBlocked = true;
|
|
251
498
|
context.state = 'blocked';
|
|
252
499
|
context.message = 'Another Felan process owns the memory writer';
|
|
253
500
|
return;
|
|
254
501
|
}
|
|
502
|
+
context.writerBlocked = false;
|
|
255
503
|
const abort = new AbortController();
|
|
256
504
|
context.abort = abort;
|
|
257
505
|
const abortOnLeaseCompromise = () => {
|
|
@@ -259,13 +507,34 @@ export class LocalMemoryCoordinator {
|
|
|
259
507
|
};
|
|
260
508
|
lease.compromised.addEventListener('abort', abortOnLeaseCompromise, { once: true });
|
|
261
509
|
let staging;
|
|
510
|
+
let retainedRun;
|
|
511
|
+
let published = false;
|
|
512
|
+
let attempt;
|
|
262
513
|
try {
|
|
263
|
-
const
|
|
514
|
+
const control = await this.#reconcileRunDiagnostics(context, lease);
|
|
515
|
+
if (control.autoDisabled) {
|
|
516
|
+
context.state = 'disabled';
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (!bypassRetryAt && control.nextRetryAt && Date.parse(control.nextRetryAt) > this.#now()) {
|
|
520
|
+
context.state = 'scheduled';
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const snapshot = await context.store.processingSnapshot(this.#options.batchSize ?? 8, (checkpoint) => !isBlockedCheckpoint(context, checkpoint));
|
|
264
524
|
if (snapshot.checkpoints.length === 0) {
|
|
265
525
|
context.state = 'idle';
|
|
266
526
|
return;
|
|
267
527
|
}
|
|
268
|
-
|
|
528
|
+
retainedRun = await LocalMemoryRun.create({
|
|
529
|
+
projectDirectory: context.store.projectDirectory,
|
|
530
|
+
sessionDirectory: join(this.#options.sessionDir ?? join(this.#options.agentDir, 'sessions'), 'memory'),
|
|
531
|
+
projectKey: context.project.key,
|
|
532
|
+
projectRoot: context.project.canonicalRoot,
|
|
533
|
+
checkpoints: snapshot.checkpoints.map(({ checkpoint }) => checkpoint),
|
|
534
|
+
baseFingerprint: snapshot.fingerprint,
|
|
535
|
+
...(control.lastDisabledRunId ? { protectedRunIds: [control.lastDisabledRunId] } : {}),
|
|
536
|
+
});
|
|
537
|
+
staging = retainedRun.workspace;
|
|
269
538
|
const memoryDirectory = join(staging, '.memory');
|
|
270
539
|
await hydrateMemoryDirectory(snapshot.artifact, memoryDirectory, { memoryPath: '.memory', mode: 'read' });
|
|
271
540
|
const manifest = await materializeMemoryInput({
|
|
@@ -287,8 +556,24 @@ export class LocalMemoryCoordinator {
|
|
|
287
556
|
if (processedCheckpoints.length === 0) {
|
|
288
557
|
context.state = 'blocked';
|
|
289
558
|
context.message = MEMORY_INPUT_BLOCKED_MESSAGE;
|
|
559
|
+
await retainedRun.finish('blocked', MEMORY_INPUT_BLOCKED_MESSAGE);
|
|
290
560
|
return;
|
|
291
561
|
}
|
|
562
|
+
await retainedRun.record({ phase: 'model', checkpoints: processedCheckpoints.map(({ checkpoint }) => checkpoint) });
|
|
563
|
+
if (abort.signal.aborted || !this.#enabled || this.#disposed)
|
|
564
|
+
throw new Error('Memory processing was cancelled');
|
|
565
|
+
attempt = await context.store.beginAttempt(lease, {
|
|
566
|
+
runId: retainedRun.metadata.sessionId,
|
|
567
|
+
baseFingerprint: snapshot.fingerprint,
|
|
568
|
+
checkpoints: processedCheckpoints,
|
|
569
|
+
...(bypassRetryAt ? { bypassRetryAt: true } : {}),
|
|
570
|
+
}, this.#now());
|
|
571
|
+
if (!attempt) {
|
|
572
|
+
await retainedRun.finish('cancelled', 'Memory inputs or processing policy changed before the attempt started');
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
context.attempt = attempt;
|
|
576
|
+
this.#emitStatusChange();
|
|
292
577
|
const result = await this.#dreamRunner({
|
|
293
578
|
stagingDirectory: staging,
|
|
294
579
|
memoryDirectory,
|
|
@@ -297,25 +582,28 @@ export class LocalMemoryCoordinator {
|
|
|
297
582
|
manifest,
|
|
298
583
|
modelRuntime: this.#options.modelRuntime,
|
|
299
584
|
...(this.#selectedModel === undefined ? {} : { selectedModel: this.#selectedModel }),
|
|
585
|
+
sessionDirectory: join(this.#options.sessionDir ?? join(this.#options.agentDir, 'sessions'), 'memory'),
|
|
586
|
+
run: retainedRun,
|
|
300
587
|
signal: abort.signal,
|
|
301
588
|
});
|
|
589
|
+
if (abort.signal.aborted || !this.#enabled || this.#disposed)
|
|
590
|
+
throw new Error('Memory processing was cancelled');
|
|
302
591
|
if (result)
|
|
303
592
|
await hydrateMemoryDirectory(result, memoryDirectory, { replace: true, memoryPath: '.memory' });
|
|
593
|
+
await retainedRun.record({ phase: 'validate' });
|
|
304
594
|
const artifact = await readMemoryDirectory(memoryDirectory, {
|
|
305
595
|
memoryPath: '.memory',
|
|
306
596
|
sourceSessionIds: allowedSourceIds(snapshot, manifest),
|
|
307
597
|
requireSources: true,
|
|
308
|
-
validateNavigation: true,
|
|
309
598
|
});
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
requireSources: true,
|
|
314
|
-
validateNavigation: true,
|
|
599
|
+
await retainedRun.record({
|
|
600
|
+
phase: 'publish',
|
|
601
|
+
outputFingerprint: createMemorySnapshot(artifact, '.memory').fingerprint,
|
|
315
602
|
});
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
await context.store.
|
|
603
|
+
await context.store.commit(lease, snapshot.fingerprint, artifact, processedCheckpoints, attempt);
|
|
604
|
+
published = true;
|
|
605
|
+
await context.store.finishAttempt(lease, attempt, { status: 'success' }, this.#now());
|
|
606
|
+
await retainedRun.finish('completed');
|
|
319
607
|
if (manifest.failures.length > 0) {
|
|
320
608
|
context.state = 'blocked';
|
|
321
609
|
context.message = MEMORY_INPUT_BLOCKED_MESSAGE;
|
|
@@ -326,22 +614,76 @@ export class LocalMemoryCoordinator {
|
|
|
326
614
|
this.#emitStatusChange();
|
|
327
615
|
}
|
|
328
616
|
catch (error) {
|
|
617
|
+
const leaseValid = await lease.verify();
|
|
618
|
+
const explicitlyCancelled = !this.#enabled || this.#disposed
|
|
619
|
+
|| (abort.signal.aborted && !lease.compromised.aborted);
|
|
620
|
+
const unavailableModel = error instanceof MemoryModelUnavailableError;
|
|
621
|
+
const blockedBeforeInference = retainedRun !== undefined && attempt === undefined && !explicitlyCancelled;
|
|
622
|
+
if (attempt && (leaseValid || explicitlyCancelled)) {
|
|
623
|
+
try {
|
|
624
|
+
const control = await context.store.finishAttempt(explicitlyCancelled || unavailableModel ? undefined : lease, attempt, explicitlyCancelled || unavailableModel ? { status: 'cancelled' }
|
|
625
|
+
: { status: 'failure', reason: sanitizeMemoryDiagnostic(error) }, this.#now());
|
|
626
|
+
published ||= control.lastOutcome?.runId === attempt.runId && control.lastOutcome.status === 'success';
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
context.storageBlocked = true;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (published) {
|
|
633
|
+
context.state = 'idle';
|
|
634
|
+
context.message = 'Memory was published, but its diagnostics could not be fully saved';
|
|
635
|
+
this.#emitStatusChange();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
329
638
|
context.state = 'error';
|
|
330
|
-
context.message = !
|
|
639
|
+
context.message = !leaseValid
|
|
331
640
|
? safeProcessingMessage(new Error('Memory writer lease was lost'))
|
|
332
641
|
: safeProcessingMessage(error);
|
|
642
|
+
if (unavailableModel || blockedBeforeInference) {
|
|
643
|
+
context.state = 'blocked';
|
|
644
|
+
for (const checkpoint of retainedRun?.metadata.checkpoints ?? []) {
|
|
645
|
+
context.blocked.set(checkpoint.sessionId, checkpoint);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (!retainedRun) {
|
|
649
|
+
context.storageBlocked = true;
|
|
650
|
+
context.message = `Memory processing is paused: ${sanitizeMemoryDiagnostic(error)}`;
|
|
651
|
+
}
|
|
652
|
+
await retainedRun?.finish(explicitlyCancelled ? 'cancelled'
|
|
653
|
+
: unavailableModel || blockedBeforeInference ? 'blocked'
|
|
654
|
+
: !leaseValid ? 'interrupted' : 'failed', error).catch(() => {
|
|
655
|
+
context.storageBlocked = true;
|
|
656
|
+
context.message = 'Memory diagnostics could not be saved; evidence remains pending';
|
|
657
|
+
});
|
|
658
|
+
this.#emitStatusChange();
|
|
333
659
|
}
|
|
334
660
|
finally {
|
|
335
661
|
lease.compromised.removeEventListener('abort', abortOnLeaseCompromise);
|
|
336
662
|
context.abort = undefined;
|
|
663
|
+
context.attempt = undefined;
|
|
337
664
|
if (staging)
|
|
338
|
-
await rm(staging, { recursive: true, force: true }).catch(() => { });
|
|
665
|
+
await rm(join(staging, '.pi-memory-runtime'), { recursive: true, force: true }).catch(() => { });
|
|
339
666
|
await lease.release();
|
|
340
667
|
}
|
|
341
668
|
}
|
|
669
|
+
async #reconcileRunDiagnostics(context, lease) {
|
|
670
|
+
let control = await context.store.readControl();
|
|
671
|
+
control = await context.store.reconcileAbandonedAttempt(lease, this.#now());
|
|
672
|
+
const options = {
|
|
673
|
+
projectDirectory: context.store.projectDirectory,
|
|
674
|
+
projectKey: context.project.key,
|
|
675
|
+
sessionDirectory: join(this.#options.sessionDir ?? join(this.#options.agentDir, 'sessions'), 'memory'),
|
|
676
|
+
...(control.lastDisabledRunId ? { protectedRunIds: [control.lastDisabledRunId] } : {}),
|
|
677
|
+
};
|
|
678
|
+
if (control.lastOutcome)
|
|
679
|
+
await LocalMemoryRun.reconcile(options, control.lastOutcome);
|
|
680
|
+
await LocalMemoryRun.reconcileAbandoned(options, [
|
|
681
|
+
...(control.activeAttempt ? [control.activeAttempt.runId] : []),
|
|
682
|
+
...(control.lastOutcome ? [control.lastOutcome.runId] : []),
|
|
683
|
+
], this.#now());
|
|
684
|
+
return control;
|
|
685
|
+
}
|
|
342
686
|
async #recover(context) {
|
|
343
|
-
if (!this.#enabled)
|
|
344
|
-
return;
|
|
345
687
|
const sessionDir = context.sessionDir ?? join(this.#options.agentDir, 'sessions');
|
|
346
688
|
let sessions;
|
|
347
689
|
try {
|
|
@@ -349,26 +691,33 @@ export class LocalMemoryCoordinator {
|
|
|
349
691
|
}
|
|
350
692
|
catch {
|
|
351
693
|
context.message = 'Memory startup recovery could not inspect sessions';
|
|
352
|
-
return;
|
|
694
|
+
return true;
|
|
353
695
|
}
|
|
696
|
+
if (this.#disposed || !this.#enabled)
|
|
697
|
+
return false;
|
|
354
698
|
let recovered = false;
|
|
355
699
|
const stableAfter = this.#options.recoveryStableMs ?? 30_000;
|
|
356
700
|
for (const info of sessions) {
|
|
357
|
-
if (
|
|
701
|
+
if (this.#disposed || !this.#enabled)
|
|
702
|
+
return false;
|
|
703
|
+
if (info.parentSessionPath || this.#now() - info.modified.getTime() < stableAfter)
|
|
358
704
|
continue;
|
|
359
705
|
try {
|
|
360
706
|
const manager = SessionManager.open(info.path, sessionDir, context.cwd);
|
|
361
707
|
const checkpoint = createSessionCheckpoint(manager);
|
|
362
708
|
if (!checkpoint)
|
|
363
709
|
continue;
|
|
710
|
+
if (this.#disposed || !this.#enabled)
|
|
711
|
+
return false;
|
|
364
712
|
recovered = (await context.store.recordCheckpoint(checkpoint)) || recovered;
|
|
365
713
|
}
|
|
366
714
|
catch { }
|
|
367
715
|
}
|
|
368
|
-
if (recovered) {
|
|
369
|
-
await this.#
|
|
716
|
+
if (recovered && !this.#disposed && this.#enabled) {
|
|
717
|
+
await this.#trackScheduleIfPending(context);
|
|
370
718
|
this.#emitStatusChange();
|
|
371
719
|
}
|
|
720
|
+
return true;
|
|
372
721
|
}
|
|
373
722
|
#emitStatusChange() {
|
|
374
723
|
for (const listener of [...this.#statusListeners]) {
|
|
@@ -379,10 +728,10 @@ export class LocalMemoryCoordinator {
|
|
|
379
728
|
}
|
|
380
729
|
}
|
|
381
730
|
#cancelScheduledRun(context) {
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
clearTimeout(context.timer);
|
|
731
|
+
if (context.timer)
|
|
732
|
+
clearTimeout(context.timer);
|
|
385
733
|
context.timer = undefined;
|
|
734
|
+
context.timerDueAt = undefined;
|
|
386
735
|
}
|
|
387
736
|
}
|
|
388
737
|
class LocalMemorySessionHost {
|