Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ jobs:
run: npm run check:openapi

- name: Code health (fallow)
run: npx fallow --no-cache
# Pinned to the last version main's CI passed cleanly with. Newer
# versions (2.45.0) flag pre-existing complexity on untouched files.
# Bump deliberately alongside a pass to address flagged issues.
run: npx fallow@2.43.0 --no-cache

- name: Run tests
run: npm test
5 changes: 4 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ jobs:
run: npx tsc --noEmit

- name: Code health (fallow)
run: npx fallow --no-cache
# Pinned to the last version main's CI passed cleanly with. Newer
# versions (2.45.0) flag pre-existing complexity on untouched files.
# Bump deliberately alongside a pass to address flagged issues.
run: npx fallow@2.43.0 --no-cache

- name: Run tests
run: npm test
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/memory-route-config-seam.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ describe('memory route config seam', () => {
});
expect(putRes.status).toBe(200);
const putBody = await putRes.json();
expect(putBody.applied).toContain('maxSearchResults');
expect(putBody.applied).toContain('max_search_results');
expect(putBody.config.max_search_results).toBe(7);

const updatedHealthRes = await fetch(`${booted.baseUrl}/memories/health`);
Expand Down
15 changes: 8 additions & 7 deletions src/__tests__/route-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ describe('POST /memories/ingest/quick — skip_extraction (storeVerbatim)', () =
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.memoriesStored).toBe(1);
expect(body.memoryIds).toHaveLength(1);
expect(body.memories_stored).toBe(1);
expect(body.stored_memory_ids).toHaveLength(1);
expect(body.updated_memory_ids).toHaveLength(0);
});
});

Expand Down Expand Up @@ -131,9 +132,9 @@ describe('POST /memories/search — scope and observability contract', () => {

expect(res.status).toBe(200);
const body = await res.json();
expect(body.scope).toEqual({ kind: 'user', userId: TEST_USER });
expect(body.scope).toEqual({ kind: 'user', user_id: TEST_USER });
expect(body.observability?.retrieval).toBeUndefined();
expect(body.observability?.packaging?.packageType).toBe('subject-pack');
expect(body.observability?.packaging?.package_type).toBe('subject-pack');
expect(body.observability?.assembly?.blocks).toEqual(['subject']);
});

Expand All @@ -156,9 +157,9 @@ describe('POST /memories/search — scope and observability contract', () => {
const body = await res.json();
expect(body.scope).toEqual({
kind: 'workspace',
userId: TEST_USER,
workspaceId,
agentId,
user_id: TEST_USER,
workspace_id: workspaceId,
agent_id: agentId,
});
});
});
Expand Down
4 changes: 2 additions & 2 deletions src/app/__tests__/composed-boot-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe('composed boot parity', () => {
// would either 500 or return a different shape.
expect(composedBody).toEqual(referenceBody);
expect(typeof composedBody.count).toBe('number');
expect(composedBody.sourceDistribution).toBeDefined();
expect(composedBody.source_distribution).toBeDefined();
});

// Runs last so any failure of the finally{} cleanup cannot bleed into
Expand All @@ -118,7 +118,7 @@ describe('composed boot parity', () => {
});
expect(putRes.status).toBe(200);
const putBody = await putRes.json();
expect(putBody.applied).toContain('maxSearchResults');
expect(putBody.applied).toContain('max_search_results');
expect(putBody.config.max_search_results).toBe(sentinel);

// The mutation goes through the composed app's PUT route into the
Expand Down
9 changes: 6 additions & 3 deletions src/app/__tests__/research-consumption-seams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ describe('Phase 6 research-consumption seams', () => {
});
expect(ingestRes.status).toBe(200);
const ingestBody = await ingestRes.json();
expect(ingestBody.memoriesStored).toBeGreaterThan(0);
expect(ingestBody.memories_stored).toBeGreaterThan(0);

const searchRes = await fetch(`${server.baseUrl}/v1/memories/search`, {
method: 'POST',
Expand Down Expand Up @@ -154,8 +154,11 @@ describe('Phase 6 research-consumption seams', () => {
body: JSON.stringify({ user_id: TEST_USER, conversation: CONVERSATION, source_site: 'test-site' }),
});
const ingestBody = await ingestRes.json();
expect(ingestBody.memoriesStored).toBeGreaterThan(0);
const writtenIds = new Set<string>(ingestBody.memoryIds);
expect(ingestBody.memories_stored).toBeGreaterThan(0);
const writtenIds = new Set<string>([
...ingestBody.stored_memory_ids,
...ingestBody.updated_memory_ids,
]);

const read = await runtime.services.memory.search(TEST_USER, 'What stack does the user use?');
const overlap = read.memories.filter((memory) => writtenIds.has(memory.id));
Expand Down
69 changes: 46 additions & 23 deletions src/routes/memories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ import { Router, type Request, type Response } from 'express';
import { config, updateRuntimeConfig, type EmbeddingProviderName, type LLMProviderName } from '../config.js';
import { MemoryService, type RetrievalResult } from '../services/memory-service.js';
import type { MemoryScope, RetrievalObservability } from '../services/memory-service-types.js';
import {
formatIngestResponse,
formatScope,
formatStatsResponse,
formatConsolidateResponse,
formatConsolidateExecuteResponse,
formatDecayResponse,
formatCapResponse,
formatLessonStatsResponse,
formatReconciliationResponse,
formatResetSourceResponse,
formatMutationSummaryResponse,
formatAuditTrailEntry,
formatObservability,
} from './memory-response-formatters.js';
import type { AgentScope, WorkspaceContext } from '../db/repository-types.js';
import { handleRouteError } from './route-errors.js';
import { validateBody, validateQuery, validateParams } from '../middleware/validate.js';
Expand Down Expand Up @@ -132,7 +147,7 @@ function registerIngestRoute(router: Router, service: MemoryService): void {
const result = body.workspace
? await service.workspaceIngest(body.userId, body.conversation, body.sourceSite, body.sourceUrl, body.workspace)
: await service.ingest(body.userId, body.conversation, body.sourceSite, body.sourceUrl);
res.json(result);
res.json(formatIngestResponse(result));
} catch (err) {
handleRouteError(res, 'POST /v1/memories/ingest', err);
}
Expand All @@ -148,7 +163,7 @@ function registerQuickIngestRoute(router: Router, service: MemoryService): void
: body.skipExtraction
? await service.storeVerbatim(body.userId, body.conversation, body.sourceSite, body.sourceUrl)
: await service.quickIngest(body.userId, body.conversation, body.sourceSite, body.sourceUrl);
res.json(result);
res.json(formatIngestResponse(result));
} catch (err) {
handleRouteError(res, 'POST /v1/memories/ingest/quick', err);
}
Expand Down Expand Up @@ -264,7 +279,8 @@ function registerStatsRoute(router: Router, service: MemoryService): void {
router.get('/stats', validateQuery(UserIdQuerySchema), async (req: Request, res: Response) => {
try {
const { userId } = req.query as unknown as { userId: string };
res.json(await service.getStats(userId));
const stats = await service.getStats(userId);
res.json(formatStatsResponse(stats));
} catch (err) {
handleRouteError(res, 'GET /v1/memories/stats', err);
}
Expand Down Expand Up @@ -304,7 +320,7 @@ function registerConfigRoute(router: Router, configRouteAdapter: RuntimeConfigRo
maxSearchResults: body.max_search_results as number | undefined,
});
res.json({
applied,
applied: applied.map(toSnakeCase),
config: formatHealthConfig(configRouteAdapter.current()),
note: 'Threshold updates applied in-memory for local experimentation. Provider/model selection is startup-only — restart the process to change it.',
});
Expand All @@ -318,10 +334,11 @@ function registerConsolidateRoute(router: Router, service: MemoryService): void
router.post('/consolidate', validateBody(ConsolidateBodySchema), async (req: Request, res: Response) => {
try {
const { userId, execute } = req.body as { userId: string; execute: boolean };
const result = execute
? await service.executeConsolidation(userId)
: await service.consolidate(userId);
res.json(result);
res.json(
execute
? formatConsolidateExecuteResponse(await service.executeConsolidation(userId))
: formatConsolidateResponse(await service.consolidate(userId)),
);
} catch (err) {
handleRouteError(res, 'POST /v1/memories/consolidate', err);
}
Expand All @@ -333,12 +350,10 @@ function registerDecayRoute(router: Router, service: MemoryService): void {
try {
const { userId, dryRun } = req.body as { userId: string; dryRun: boolean };
const result = await service.evaluateDecay(userId);
if (!dryRun && result.candidatesForArchival.length > 0) {
const archived = await service.archiveDecayed(userId, result.candidatesForArchival.map((c) => c.id));
res.json({ ...result, archived });
return;
}
res.json({ ...result, archived: 0 });
const archived = !dryRun && result.candidatesForArchival.length > 0
? await service.archiveDecayed(userId, result.candidatesForArchival.map((c) => c.id))
: 0;
res.json(formatDecayResponse(result, archived));
} catch (err) {
handleRouteError(res, 'POST /v1/memories/decay', err);
}
Expand All @@ -350,7 +365,7 @@ function registerCapRoute(router: Router, service: MemoryService): void {
try {
const { userId } = req.query as unknown as { userId: string };
const result = await service.checkCap(userId);
res.json(result);
res.json(formatCapResponse(result));
} catch (err) {
handleRouteError(res, 'GET /v1/memories/cap', err);
}
Expand All @@ -372,7 +387,7 @@ function registerLessonRoutes(router: Router, service: MemoryService): void {
try {
const { userId } = req.query as unknown as { userId: string };
const stats = await service.getLessonStats(userId);
res.json(stats);
res.json(formatLessonStatsResponse(stats));
} catch (err) {
handleRouteError(res, 'GET /v1/memories/lessons/stats', err);
}
Expand All @@ -387,7 +402,7 @@ function registerLessonRoutes(router: Router, service: MemoryService): void {
severity: unknown;
};
const lessonId = await service.reportLesson(userId, pattern, sourceMemoryIds, severity as never);
res.json({ lessonId });
res.json({ lesson_id: lessonId });
} catch (err) {
handleRouteError(res, 'POST /v1/memories/lessons/report', err);
}
Expand Down Expand Up @@ -417,7 +432,7 @@ function registerReconcileRoute(router: Router, service: MemoryService): void {
const result = userId
? await service.reconcileDeferred(userId)
: await service.reconcileDeferredAll();
res.json(result);
res.json(formatReconciliationResponse(result));
} catch (err) {
handleRouteError(res, 'POST /v1/memories/reconcile', err);
}
Expand All @@ -439,7 +454,7 @@ function registerResetSourceRoute(router: Router, service: MemoryService): void
try {
const { userId, sourceSite } = req.body as { userId: string; sourceSite: string };
const result = await service.resetBySource(userId, sourceSite);
res.json({ success: true, ...result });
res.json(formatResetSourceResponse(result));
} catch (err) {
handleRouteError(res, 'POST /v1/memories/reset-source', err);
}
Expand Down Expand Up @@ -515,7 +530,7 @@ function registerAuditSummaryRoute(router: Router, service: MemoryService): void
try {
const { userId } = req.query as unknown as { userId: string };
const summary = await service.getMutationSummary(userId);
res.json(summary);
res.json(formatMutationSummaryResponse(summary));
} catch (err) {
handleRouteError(res, 'GET /v1/memories/audit/summary', err);
}
Expand Down Expand Up @@ -544,7 +559,11 @@ function registerAuditTrailRoute(router: Router, service: MemoryService): void {
const { id: memoryId } = req.params as unknown as { id: string };
const { userId } = req.query as unknown as { userId: string };
const trail = await service.getAuditTrail(userId, memoryId);
res.json({ memoryId, trail, versionCount: trail.length });
res.json({
memory_id: memoryId,
trail: trail.map(formatAuditTrailEntry),
version_count: trail.length,
});
} catch (err) {
handleRouteError(res, 'GET /v1/memories/:id/audit', err);
}
Expand Down Expand Up @@ -607,6 +626,10 @@ function readRuntimeConfigRouteSnapshot(): RuntimeConfigRouteSnapshot {
};
}

function toSnakeCase(camel: string): string {
return camel.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
}

function formatHealthConfig(runtimeConfig: RuntimeConfigRouteSnapshot) {
return {
retrieval_profile: runtimeConfig.retrievalProfile,
Expand All @@ -630,7 +653,7 @@ function formatSearchResponse(result: RetrievalResult, scope: MemoryScope) {
return {
count: result.memories.length,
retrieval_mode: result.retrievalMode,
scope,
scope: formatScope(scope),
memories: result.memories.map((memory) => ({
id: memory.id,
content: memory.content,
Expand Down Expand Up @@ -669,6 +692,6 @@ function formatSearchResponse(result: RetrievalResult, scope: MemoryScope) {
removed_memory_ids: result.consensusResult.removedMemoryIds,
},
} : {}),
...(observability ? { observability } : {}),
...(observability ? { observability: formatObservability(observability) } : {}),
};
}
Loading
Loading