@soleri/core 9.4.0 → 9.5.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/dist/hooks/candidate-scorer.d.ts +28 -0
- package/dist/hooks/candidate-scorer.d.ts.map +1 -0
- package/dist/hooks/candidate-scorer.js +20 -0
- package/dist/hooks/candidate-scorer.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +6 -1
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/package.json +1 -1
- package/src/brain/brain.ts +120 -46
- package/src/brain/intelligence.ts +42 -34
- package/src/chat/agent-loop.ts +1 -1
- package/src/chat/notifications.ts +4 -0
- package/src/control/intent-router.ts +10 -8
- package/src/curator/curator.ts +145 -29
- package/src/hooks/candidate-scorer.test.ts +76 -0
- package/src/hooks/candidate-scorer.ts +39 -0
- package/src/hooks/index.ts +6 -0
- package/src/index.ts +2 -0
- package/src/llm/llm-client.ts +1 -0
- package/src/persistence/sqlite-provider.ts +1 -0
- package/src/planning/github-projection.ts +48 -44
- package/src/planning/plan-lifecycle.ts +14 -1
- package/src/queue/pipeline-runner.ts +4 -0
- package/src/runtime/curator-extra-ops.test.ts +7 -0
- package/src/runtime/curator-extra-ops.ts +10 -1
- package/src/runtime/facades/curator-facade.test.ts +7 -0
- package/src/runtime/facades/memory-facade.ts +187 -0
- package/src/runtime/orchestrate-ops.ts +3 -3
- package/src/runtime/runtime.test.ts +50 -2
- package/src/runtime/runtime.ts +117 -89
- package/src/runtime/shutdown-registry.test.ts +151 -0
- package/src/runtime/shutdown-registry.ts +85 -0
- package/src/runtime/types.ts +4 -1
- package/src/transport/http-server.ts +50 -3
- package/src/transport/ws-server.ts +8 -0
- package/src/vault/linking.test.ts +12 -0
- package/src/vault/linking.ts +90 -44
- package/src/vault/vault-maintenance.ts +11 -18
- package/src/vault/vault-memories.ts +21 -13
- package/src/vault/vault-schema.ts +21 -0
- package/src/vault/vault.ts +8 -3
|
@@ -10,9 +10,18 @@ import type { OpDefinition } from '../facades/types.js';
|
|
|
10
10
|
import type { AgentRuntime } from './types.js';
|
|
11
11
|
|
|
12
12
|
export function createCuratorExtraOps(runtime: AgentRuntime): OpDefinition[] {
|
|
13
|
-
const { curator, jobQueue, pipelineRunner } = runtime;
|
|
13
|
+
const { curator, jobQueue, pipelineRunner, shutdownRegistry } = runtime;
|
|
14
14
|
let consolidationInterval: ReturnType<typeof setInterval> | null = null;
|
|
15
15
|
|
|
16
|
+
// Register cleanup for any consolidation interval started during this session
|
|
17
|
+
shutdownRegistry.register('curatorConsolidation', () => {
|
|
18
|
+
if (consolidationInterval) {
|
|
19
|
+
clearInterval(consolidationInterval);
|
|
20
|
+
consolidationInterval = null;
|
|
21
|
+
}
|
|
22
|
+
pipelineRunner.stop();
|
|
23
|
+
});
|
|
24
|
+
|
|
16
25
|
return [
|
|
17
26
|
// ─── Entry History ──────────────────────────────────────────────
|
|
18
27
|
{
|
|
@@ -30,6 +30,13 @@ function mockRuntime(): AgentRuntime {
|
|
|
30
30
|
start: vi.fn(),
|
|
31
31
|
stop: vi.fn(),
|
|
32
32
|
},
|
|
33
|
+
shutdownRegistry: {
|
|
34
|
+
register: vi.fn(),
|
|
35
|
+
closeAll: vi.fn(),
|
|
36
|
+
closeAllSync: vi.fn(),
|
|
37
|
+
size: 0,
|
|
38
|
+
isClosed: false,
|
|
39
|
+
},
|
|
33
40
|
} as unknown as AgentRuntime;
|
|
34
41
|
}
|
|
35
42
|
|
|
@@ -195,6 +195,193 @@ export function createMemoryFacadeOps(runtime: AgentRuntime): OpDefinition[] {
|
|
|
195
195
|
},
|
|
196
196
|
},
|
|
197
197
|
|
|
198
|
+
// ─── Handoff ────────────────────────────────────────────────
|
|
199
|
+
{
|
|
200
|
+
name: 'handoff_generate',
|
|
201
|
+
description:
|
|
202
|
+
'Generate a structured handoff document for context transitions. ' +
|
|
203
|
+
'Pulls from active plan (if any) and recent session memories to produce ' +
|
|
204
|
+
'a markdown document that can bootstrap a new context window. ' +
|
|
205
|
+
'Ephemeral — NOT persisted to vault.',
|
|
206
|
+
auth: 'read',
|
|
207
|
+
schema: z.object({
|
|
208
|
+
projectPath: z
|
|
209
|
+
.string()
|
|
210
|
+
.optional()
|
|
211
|
+
.default('.')
|
|
212
|
+
.describe('Project path for filtering memories'),
|
|
213
|
+
sessionLimit: z
|
|
214
|
+
.number()
|
|
215
|
+
.optional()
|
|
216
|
+
.default(3)
|
|
217
|
+
.describe('Number of recent session memories to include'),
|
|
218
|
+
}),
|
|
219
|
+
handler: async (params) => {
|
|
220
|
+
const { planner } = runtime;
|
|
221
|
+
const projectPath = params.projectPath as string;
|
|
222
|
+
const sessionLimit = (params.sessionLimit as number) ?? 3;
|
|
223
|
+
|
|
224
|
+
const sections: string[] = [];
|
|
225
|
+
const now = new Date().toISOString();
|
|
226
|
+
|
|
227
|
+
sections.push('# Handoff Document');
|
|
228
|
+
sections.push('');
|
|
229
|
+
sections.push(`Generated: ${now}`);
|
|
230
|
+
sections.push('');
|
|
231
|
+
|
|
232
|
+
// ─── Active Plan Context ───────────────────────────
|
|
233
|
+
const activePlans = planner.getActive();
|
|
234
|
+
if (activePlans.length > 0) {
|
|
235
|
+
const plan = activePlans[0]; // Most relevant active plan
|
|
236
|
+
sections.push('## Active Plan');
|
|
237
|
+
sections.push('');
|
|
238
|
+
sections.push(`| Field | Value |`);
|
|
239
|
+
sections.push(`|-------|-------|`);
|
|
240
|
+
sections.push(`| **Plan ID** | ${plan.id} |`);
|
|
241
|
+
sections.push(`| **Objective** | ${plan.objective} |`);
|
|
242
|
+
sections.push(`| **Status** | ${plan.status} |`);
|
|
243
|
+
sections.push(`| **Scope** | ${plan.scope} |`);
|
|
244
|
+
sections.push('');
|
|
245
|
+
|
|
246
|
+
// Decisions
|
|
247
|
+
if (plan.decisions.length > 0) {
|
|
248
|
+
sections.push('### Decisions');
|
|
249
|
+
sections.push('');
|
|
250
|
+
for (const d of plan.decisions) {
|
|
251
|
+
if (typeof d === 'string') {
|
|
252
|
+
sections.push(`- ${d}`);
|
|
253
|
+
} else {
|
|
254
|
+
sections.push(`- **${d.decision}** — ${d.rationale}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
sections.push('');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Task status summary
|
|
261
|
+
if (plan.tasks.length > 0) {
|
|
262
|
+
sections.push('### Tasks');
|
|
263
|
+
sections.push('');
|
|
264
|
+
sections.push('| # | Task | Status |');
|
|
265
|
+
sections.push('|---|------|--------|');
|
|
266
|
+
for (let i = 0; i < plan.tasks.length; i++) {
|
|
267
|
+
const t = plan.tasks[i];
|
|
268
|
+
sections.push(`| ${i + 1} | ${t.title} | ${t.status} |`);
|
|
269
|
+
}
|
|
270
|
+
sections.push('');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Approach
|
|
274
|
+
if (plan.approach) {
|
|
275
|
+
sections.push('### Approach');
|
|
276
|
+
sections.push('');
|
|
277
|
+
sections.push(plan.approach);
|
|
278
|
+
sections.push('');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Additional active plans (just IDs)
|
|
282
|
+
if (activePlans.length > 1) {
|
|
283
|
+
sections.push('### Other Active Plans');
|
|
284
|
+
sections.push('');
|
|
285
|
+
for (let i = 1; i < activePlans.length; i++) {
|
|
286
|
+
const p = activePlans[i];
|
|
287
|
+
sections.push(`- **${p.id}**: ${p.objective} (${p.status})`);
|
|
288
|
+
}
|
|
289
|
+
sections.push('');
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
sections.push('## Active Plan');
|
|
293
|
+
sections.push('');
|
|
294
|
+
sections.push('No active plans.');
|
|
295
|
+
sections.push('');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ─── Recent Session Context ────────────────────────
|
|
299
|
+
const recentSessions = vault.listMemories({
|
|
300
|
+
type: 'session',
|
|
301
|
+
projectPath,
|
|
302
|
+
limit: sessionLimit,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (recentSessions.length > 0) {
|
|
306
|
+
sections.push('## Recent Sessions');
|
|
307
|
+
sections.push('');
|
|
308
|
+
for (const session of recentSessions) {
|
|
309
|
+
sections.push(`### ${session.createdAt}`);
|
|
310
|
+
sections.push('');
|
|
311
|
+
if (session.summary) {
|
|
312
|
+
sections.push(session.summary);
|
|
313
|
+
sections.push('');
|
|
314
|
+
}
|
|
315
|
+
if (session.nextSteps && session.nextSteps.length > 0) {
|
|
316
|
+
sections.push('**Next steps:**');
|
|
317
|
+
for (const step of session.nextSteps) {
|
|
318
|
+
sections.push(`- ${step}`);
|
|
319
|
+
}
|
|
320
|
+
sections.push('');
|
|
321
|
+
}
|
|
322
|
+
if (session.decisions && session.decisions.length > 0) {
|
|
323
|
+
sections.push('**Decisions:**');
|
|
324
|
+
for (const d of session.decisions) {
|
|
325
|
+
sections.push(`- ${d}`);
|
|
326
|
+
}
|
|
327
|
+
sections.push('');
|
|
328
|
+
}
|
|
329
|
+
if (session.filesModified && session.filesModified.length > 0) {
|
|
330
|
+
sections.push(`**Files modified:** ${session.filesModified.join(', ')}`);
|
|
331
|
+
sections.push('');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
sections.push('## Recent Sessions');
|
|
336
|
+
sections.push('');
|
|
337
|
+
sections.push('No recent session memories found.');
|
|
338
|
+
sections.push('');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ─── Resumption Hints ──────────────────────────────
|
|
342
|
+
sections.push('## Resumption');
|
|
343
|
+
sections.push('');
|
|
344
|
+
sections.push('Use this document to restore context after a context window transition.');
|
|
345
|
+
sections.push('');
|
|
346
|
+
if (activePlans.length > 0) {
|
|
347
|
+
const plan = activePlans[0];
|
|
348
|
+
const pendingTasks = plan.tasks.filter(
|
|
349
|
+
(t) => t.status === 'pending' || t.status === 'in_progress',
|
|
350
|
+
);
|
|
351
|
+
if (pendingTasks.length > 0) {
|
|
352
|
+
sections.push('**Immediate next actions:**');
|
|
353
|
+
for (const t of pendingTasks.slice(0, 5)) {
|
|
354
|
+
sections.push(
|
|
355
|
+
`- ${t.status === 'in_progress' ? '[IN PROGRESS]' : '[PENDING]'} ${t.title}`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
sections.push('');
|
|
359
|
+
}
|
|
360
|
+
if (plan.status === 'executing') {
|
|
361
|
+
sections.push(
|
|
362
|
+
'> Plan is in `executing` state. Continue with pending tasks or call `op:plan_reconcile` if complete.',
|
|
363
|
+
);
|
|
364
|
+
} else if (plan.status === 'reconciling') {
|
|
365
|
+
sections.push(
|
|
366
|
+
'> Plan is in `reconciling` state. Call `op:plan_complete_lifecycle` to finalize.',
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const markdown = sections.join('\n');
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
handoff: markdown,
|
|
375
|
+
meta: {
|
|
376
|
+
activePlanCount: activePlans.length,
|
|
377
|
+
activePlanId: activePlans.length > 0 ? activePlans[0].id : null,
|
|
378
|
+
recentSessionCount: recentSessions.length,
|
|
379
|
+
generatedAt: now,
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
|
|
198
385
|
// ─── Satellite ops ───────────────────────────────────────────
|
|
199
386
|
...createMemoryExtraOps(runtime),
|
|
200
387
|
...createMemoryCrossProjectOps(runtime),
|
|
@@ -847,7 +847,7 @@ export function createOrchestrateOps(
|
|
|
847
847
|
}
|
|
848
848
|
|
|
849
849
|
// 2. Detect GitHub context
|
|
850
|
-
const ctx = detectGitHubContext(projectPath);
|
|
850
|
+
const ctx = await detectGitHubContext(projectPath);
|
|
851
851
|
if (!ctx) {
|
|
852
852
|
return {
|
|
853
853
|
status: 'skipped',
|
|
@@ -885,7 +885,7 @@ export function createOrchestrateOps(
|
|
|
885
885
|
};
|
|
886
886
|
}
|
|
887
887
|
|
|
888
|
-
const updated = updateGitHubIssueBody(ctx.repo, linkToIssue, body);
|
|
888
|
+
const updated = await updateGitHubIssueBody(ctx.repo, linkToIssue, body);
|
|
889
889
|
if (!updated) {
|
|
890
890
|
return {
|
|
891
891
|
status: 'error',
|
|
@@ -949,7 +949,7 @@ export function createOrchestrateOps(
|
|
|
949
949
|
continue;
|
|
950
950
|
}
|
|
951
951
|
|
|
952
|
-
const issueNumber = createGitHubIssue(ctx.repo, task.title, body, {
|
|
952
|
+
const issueNumber = await createGitHubIssue(ctx.repo, task.title, body, {
|
|
953
953
|
milestone: milestoneNumber,
|
|
954
954
|
labels: labels.length > 0 ? labels : undefined,
|
|
955
955
|
});
|
|
@@ -81,7 +81,10 @@ vi.mock('../governance/governance.js', () => ({
|
|
|
81
81
|
}));
|
|
82
82
|
|
|
83
83
|
vi.mock('../loop/loop-manager.js', () => ({
|
|
84
|
-
LoopManager:
|
|
84
|
+
LoopManager: vi.fn(function (this: Record<string, unknown>) {
|
|
85
|
+
this.isActive = vi.fn().mockReturnValue(false);
|
|
86
|
+
this.cancelLoop = vi.fn();
|
|
87
|
+
}),
|
|
85
88
|
}));
|
|
86
89
|
|
|
87
90
|
vi.mock('../control/identity-manager.js', () => ({
|
|
@@ -185,7 +188,9 @@ vi.mock('../context/context-engine.js', () => ({
|
|
|
185
188
|
}));
|
|
186
189
|
|
|
187
190
|
vi.mock('../agency/agency-manager.js', () => ({
|
|
188
|
-
AgencyManager:
|
|
191
|
+
AgencyManager: vi.fn(function (this: Record<string, unknown>) {
|
|
192
|
+
this.disable = vi.fn();
|
|
193
|
+
}),
|
|
189
194
|
}));
|
|
190
195
|
|
|
191
196
|
vi.mock('../vault/knowledge-review.js', () => ({
|
|
@@ -220,6 +225,7 @@ vi.mock('../queue/job-queue.js', () => ({
|
|
|
220
225
|
vi.mock('../queue/pipeline-runner.js', () => ({
|
|
221
226
|
PipelineRunner: vi.fn(function (this: Record<string, unknown>) {
|
|
222
227
|
this.registerHandler = vi.fn();
|
|
228
|
+
this.stop = vi.fn();
|
|
223
229
|
}),
|
|
224
230
|
}));
|
|
225
231
|
|
|
@@ -261,6 +267,18 @@ vi.mock('./context-health.js', () => ({
|
|
|
261
267
|
}),
|
|
262
268
|
}));
|
|
263
269
|
|
|
270
|
+
vi.mock('./shutdown-registry.js', () => ({
|
|
271
|
+
ShutdownRegistry: vi.fn(function (this: Record<string, unknown>) {
|
|
272
|
+
this.register = vi.fn();
|
|
273
|
+
this.closeAll = vi.fn().mockResolvedValue(undefined);
|
|
274
|
+
this.closeAllSync = vi.fn();
|
|
275
|
+
this.size = 0;
|
|
276
|
+
this.isClosed = false;
|
|
277
|
+
this.entries = [];
|
|
278
|
+
this.closed = false;
|
|
279
|
+
}),
|
|
280
|
+
}));
|
|
281
|
+
|
|
264
282
|
vi.mock('node:fs', () => ({
|
|
265
283
|
existsSync: vi.fn().mockReturnValue(false),
|
|
266
284
|
mkdirSync: vi.fn(),
|
|
@@ -360,4 +378,34 @@ describe('createAgentRuntime', () => {
|
|
|
360
378
|
it('initializes context health monitor', () => {
|
|
361
379
|
expect(runtime.contextHealth).toBeDefined();
|
|
362
380
|
});
|
|
381
|
+
|
|
382
|
+
it('initializes shutdown registry', () => {
|
|
383
|
+
expect(runtime.shutdownRegistry).toBeDefined();
|
|
384
|
+
expect(runtime.shutdownRegistry.register).toBeDefined();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('registers cleanup callbacks with shutdown registry', () => {
|
|
388
|
+
// vaultManager, pipelineRunner, agencyManager, loopManager
|
|
389
|
+
expect(runtime.shutdownRegistry.register).toHaveBeenCalledWith(
|
|
390
|
+
'vaultManager',
|
|
391
|
+
expect.any(Function),
|
|
392
|
+
);
|
|
393
|
+
expect(runtime.shutdownRegistry.register).toHaveBeenCalledWith(
|
|
394
|
+
'pipelineRunner',
|
|
395
|
+
expect.any(Function),
|
|
396
|
+
);
|
|
397
|
+
expect(runtime.shutdownRegistry.register).toHaveBeenCalledWith(
|
|
398
|
+
'agencyManager',
|
|
399
|
+
expect.any(Function),
|
|
400
|
+
);
|
|
401
|
+
expect(runtime.shutdownRegistry.register).toHaveBeenCalledWith(
|
|
402
|
+
'loopManager',
|
|
403
|
+
expect.any(Function),
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('close() calls shutdownRegistry.closeAllSync()', () => {
|
|
408
|
+
runtime.close();
|
|
409
|
+
expect(runtime.shutdownRegistry.closeAllSync).toHaveBeenCalled();
|
|
410
|
+
});
|
|
363
411
|
});
|
package/src/runtime/runtime.ts
CHANGED
|
@@ -60,6 +60,7 @@ import { loadPersona } from '../persona/loader.js';
|
|
|
60
60
|
import { generatePersonaInstructions } from '../persona/prompt-generator.js';
|
|
61
61
|
import { OperatorProfileStore } from '../operator/operator-profile.js';
|
|
62
62
|
import { ContextHealthMonitor } from './context-health.js';
|
|
63
|
+
import { ShutdownRegistry } from './shutdown-registry.js';
|
|
63
64
|
|
|
64
65
|
/**
|
|
65
66
|
* Create a fully initialized agent runtime.
|
|
@@ -221,6 +222,117 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
|
|
|
221
222
|
learningRadar.setOperatorProfile(operatorProfile);
|
|
222
223
|
brainIntelligence.setOperatorProfile(operatorProfile);
|
|
223
224
|
|
|
225
|
+
// ─── Shutdown Registry ────────────────────────────────────────────
|
|
226
|
+
const shutdownRegistry = new ShutdownRegistry();
|
|
227
|
+
|
|
228
|
+
// Build pipeline runner before the runtime object so we can reference it
|
|
229
|
+
const pipelineRunner = (() => {
|
|
230
|
+
const jq = new JobQueue(vault.getProvider());
|
|
231
|
+
const pr = new PipelineRunner(jq);
|
|
232
|
+
// Register default job handlers for curator pipeline
|
|
233
|
+
pr.registerHandler('tag-normalize', async (job) => {
|
|
234
|
+
const entry = vault.get(job.entryId ?? '');
|
|
235
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
236
|
+
const result = curator.normalizeTag(entry.tags[0] ?? '');
|
|
237
|
+
return result;
|
|
238
|
+
});
|
|
239
|
+
pr.registerHandler('dedup-check', async (job) => {
|
|
240
|
+
const entry = vault.get(job.entryId ?? '');
|
|
241
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
242
|
+
return curator.detectDuplicates(entry.id);
|
|
243
|
+
});
|
|
244
|
+
pr.registerHandler('auto-link', async (job) => {
|
|
245
|
+
if (linkManager) {
|
|
246
|
+
const suggestions = linkManager.suggestLinks(job.entryId ?? '', 3);
|
|
247
|
+
for (const s of suggestions) {
|
|
248
|
+
linkManager.addLink(
|
|
249
|
+
job.entryId ?? '',
|
|
250
|
+
s.entryId,
|
|
251
|
+
s.suggestedType,
|
|
252
|
+
`pipeline: ${s.reason}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return { linked: suggestions.length };
|
|
256
|
+
}
|
|
257
|
+
return { skipped: true, reason: 'link manager not available' };
|
|
258
|
+
});
|
|
259
|
+
pr.registerHandler('quality-gate', async (job) => {
|
|
260
|
+
const entry = vault.get(job.entryId ?? '');
|
|
261
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
262
|
+
return evaluateQuality(entry, llmClient);
|
|
263
|
+
});
|
|
264
|
+
pr.registerHandler('classify', async (job) => {
|
|
265
|
+
const entry = vault.get(job.entryId ?? '');
|
|
266
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
267
|
+
return classifyEntry(entry, llmClient);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ─── 9 additional handlers for full Salvador parity (#216) ────
|
|
271
|
+
pr.registerHandler('enrich-frontmatter', async (job) => {
|
|
272
|
+
const entry = vault.get(job.entryId ?? '');
|
|
273
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
274
|
+
return curator.enrichMetadata(entry.id);
|
|
275
|
+
});
|
|
276
|
+
pr.registerHandler('detect-staleness', async (job) => {
|
|
277
|
+
const entry = vault.get(job.entryId ?? '');
|
|
278
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
279
|
+
// Check if entry is older than 90 days (using validFrom or fallback to 0)
|
|
280
|
+
const entryTimestamp = (entry.validFrom ?? 0) * 1000 || Date.now();
|
|
281
|
+
const ageMs = Date.now() - entryTimestamp;
|
|
282
|
+
const staleDays = 90;
|
|
283
|
+
const isStale = ageMs > staleDays * 86400000;
|
|
284
|
+
return { stale: isStale, ageDays: Math.floor(ageMs / 86400000), entryId: entry.id };
|
|
285
|
+
});
|
|
286
|
+
pr.registerHandler('detect-duplicate', async (job) => {
|
|
287
|
+
const entry = vault.get(job.entryId ?? '');
|
|
288
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
289
|
+
return curator.detectDuplicates(entry.id);
|
|
290
|
+
});
|
|
291
|
+
pr.registerHandler('detect-contradiction', async (job) => {
|
|
292
|
+
const entry = vault.get(job.entryId ?? '');
|
|
293
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
294
|
+
const contradictions = curator.detectContradictions(0.4);
|
|
295
|
+
const relevant = contradictions.filter(
|
|
296
|
+
(c) => c.patternId === job.entryId || c.antipatternId === job.entryId,
|
|
297
|
+
);
|
|
298
|
+
return { found: relevant.length, contradictions: relevant };
|
|
299
|
+
});
|
|
300
|
+
pr.registerHandler('consolidate-duplicates', async (_job) => {
|
|
301
|
+
return curator.consolidate({ dryRun: false, staleDaysThreshold: 90 });
|
|
302
|
+
});
|
|
303
|
+
pr.registerHandler('archive-stale', async (_job) => {
|
|
304
|
+
// Run consolidation with stale detection
|
|
305
|
+
const result = curator.consolidate({ dryRun: false, staleDaysThreshold: 90 });
|
|
306
|
+
return { archived: result.staleEntries.length, result };
|
|
307
|
+
});
|
|
308
|
+
pr.registerHandler('verify-searchable', async (job) => {
|
|
309
|
+
const entry = vault.get(job.entryId ?? '');
|
|
310
|
+
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
311
|
+
const searchResults = vault.search(entry.title, { limit: 1 });
|
|
312
|
+
const found = searchResults.some((r) => r.entry.id === entry.id);
|
|
313
|
+
return { searchable: found, entryId: entry.id };
|
|
314
|
+
});
|
|
315
|
+
return pr;
|
|
316
|
+
})();
|
|
317
|
+
|
|
318
|
+
// ─── Register cleanup callbacks (LIFO: first registered = last closed) ──
|
|
319
|
+
// Vault manager closes last (other modules may flush to vault during close)
|
|
320
|
+
shutdownRegistry.register('vaultManager', () => vaultManager.close());
|
|
321
|
+
// Pipeline runner — clear its polling interval
|
|
322
|
+
shutdownRegistry.register('pipelineRunner', () => pipelineRunner.stop());
|
|
323
|
+
// Agency manager — close FSWatchers and debounce timers
|
|
324
|
+
shutdownRegistry.register('agencyManager', () => agencyManager.disable());
|
|
325
|
+
// Loop manager — clear accumulated state
|
|
326
|
+
shutdownRegistry.register('loopManager', () => {
|
|
327
|
+
if (loop.isActive()) {
|
|
328
|
+
try {
|
|
329
|
+
loop.cancelLoop();
|
|
330
|
+
} catch {
|
|
331
|
+
// Loop may already be inactive
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
224
336
|
return {
|
|
225
337
|
config,
|
|
226
338
|
logger,
|
|
@@ -256,94 +368,7 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
|
|
|
256
368
|
knowledgeSynthesizer: new KnowledgeSynthesizer(brain, llmClient),
|
|
257
369
|
chainRunner: new ChainRunner(vault.getProvider()),
|
|
258
370
|
jobQueue: new JobQueue(vault.getProvider()),
|
|
259
|
-
pipelineRunner
|
|
260
|
-
const jq = new JobQueue(vault.getProvider());
|
|
261
|
-
const pr = new PipelineRunner(jq);
|
|
262
|
-
// Register default job handlers for curator pipeline
|
|
263
|
-
pr.registerHandler('tag-normalize', async (job) => {
|
|
264
|
-
const entry = vault.get(job.entryId ?? '');
|
|
265
|
-
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
266
|
-
const result = curator.normalizeTag(entry.tags[0] ?? '');
|
|
267
|
-
return result;
|
|
268
|
-
});
|
|
269
|
-
pr.registerHandler('dedup-check', async (job) => {
|
|
270
|
-
const entry = vault.get(job.entryId ?? '');
|
|
271
|
-
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
272
|
-
return curator.detectDuplicates(entry.id);
|
|
273
|
-
});
|
|
274
|
-
pr.registerHandler('auto-link', async (job) => {
|
|
275
|
-
if (linkManager) {
|
|
276
|
-
const suggestions = linkManager.suggestLinks(job.entryId ?? '', 3);
|
|
277
|
-
for (const s of suggestions) {
|
|
278
|
-
linkManager.addLink(
|
|
279
|
-
job.entryId ?? '',
|
|
280
|
-
s.entryId,
|
|
281
|
-
s.suggestedType,
|
|
282
|
-
`pipeline: ${s.reason}`,
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
return { linked: suggestions.length };
|
|
286
|
-
}
|
|
287
|
-
return { skipped: true, reason: 'link manager not available' };
|
|
288
|
-
});
|
|
289
|
-
pr.registerHandler('quality-gate', async (job) => {
|
|
290
|
-
const entry = vault.get(job.entryId ?? '');
|
|
291
|
-
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
292
|
-
return evaluateQuality(entry, llmClient);
|
|
293
|
-
});
|
|
294
|
-
pr.registerHandler('classify', async (job) => {
|
|
295
|
-
const entry = vault.get(job.entryId ?? '');
|
|
296
|
-
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
297
|
-
return classifyEntry(entry, llmClient);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// ─── 9 additional handlers for full Salvador parity (#216) ────
|
|
301
|
-
pr.registerHandler('enrich-frontmatter', async (job) => {
|
|
302
|
-
const entry = vault.get(job.entryId ?? '');
|
|
303
|
-
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
304
|
-
return curator.enrichMetadata(entry.id);
|
|
305
|
-
});
|
|
306
|
-
pr.registerHandler('detect-staleness', async (job) => {
|
|
307
|
-
const entry = vault.get(job.entryId ?? '');
|
|
308
|
-
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
309
|
-
// Check if entry is older than 90 days (using validFrom or fallback to 0)
|
|
310
|
-
const entryTimestamp = (entry.validFrom ?? 0) * 1000 || Date.now();
|
|
311
|
-
const ageMs = Date.now() - entryTimestamp;
|
|
312
|
-
const staleDays = 90;
|
|
313
|
-
const isStale = ageMs > staleDays * 86400000;
|
|
314
|
-
return { stale: isStale, ageDays: Math.floor(ageMs / 86400000), entryId: entry.id };
|
|
315
|
-
});
|
|
316
|
-
pr.registerHandler('detect-duplicate', async (job) => {
|
|
317
|
-
const entry = vault.get(job.entryId ?? '');
|
|
318
|
-
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
319
|
-
return curator.detectDuplicates(entry.id);
|
|
320
|
-
});
|
|
321
|
-
pr.registerHandler('detect-contradiction', async (job) => {
|
|
322
|
-
const entry = vault.get(job.entryId ?? '');
|
|
323
|
-
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
324
|
-
const contradictions = curator.detectContradictions(0.4);
|
|
325
|
-
const relevant = contradictions.filter(
|
|
326
|
-
(c) => c.patternId === job.entryId || c.antipatternId === job.entryId,
|
|
327
|
-
);
|
|
328
|
-
return { found: relevant.length, contradictions: relevant };
|
|
329
|
-
});
|
|
330
|
-
pr.registerHandler('consolidate-duplicates', async (_job) => {
|
|
331
|
-
return curator.consolidate({ dryRun: false, staleDaysThreshold: 90 });
|
|
332
|
-
});
|
|
333
|
-
pr.registerHandler('archive-stale', async (_job) => {
|
|
334
|
-
// Run consolidation with stale detection
|
|
335
|
-
const result = curator.consolidate({ dryRun: false, staleDaysThreshold: 90 });
|
|
336
|
-
return { archived: result.staleEntries.length, result };
|
|
337
|
-
});
|
|
338
|
-
pr.registerHandler('verify-searchable', async (job) => {
|
|
339
|
-
const entry = vault.get(job.entryId ?? '');
|
|
340
|
-
if (!entry) return { skipped: true, reason: 'entry not found' };
|
|
341
|
-
const searchResults = vault.search(entry.title, { limit: 1 });
|
|
342
|
-
const found = searchResults.some((r) => r.entry.id === entry.id);
|
|
343
|
-
return { searchable: found, entryId: entry.id };
|
|
344
|
-
});
|
|
345
|
-
return pr;
|
|
346
|
-
})(),
|
|
371
|
+
pipelineRunner,
|
|
347
372
|
operatorProfile,
|
|
348
373
|
persona: (() => {
|
|
349
374
|
const p = loadPersona(agentId, config.persona ?? undefined);
|
|
@@ -355,9 +380,12 @@ export function createAgentRuntime(config: AgentRuntimeConfig): AgentRuntime {
|
|
|
355
380
|
return generatePersonaInstructions(p);
|
|
356
381
|
})(),
|
|
357
382
|
contextHealth: new ContextHealthMonitor(),
|
|
383
|
+
shutdownRegistry,
|
|
358
384
|
createdAt: Date.now(),
|
|
359
385
|
close: () => {
|
|
360
|
-
|
|
386
|
+
// Synchronous close — runs all registered callbacks in LIFO order,
|
|
387
|
+
// then closes the vault (registered first, so runs last).
|
|
388
|
+
shutdownRegistry.closeAllSync();
|
|
361
389
|
},
|
|
362
390
|
};
|
|
363
391
|
}
|