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
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Auth uses timing-safe token comparison. Origin allowlisting supports `"*"` wildc
| `remember_context` | Single write tool for chat and admin clients: save durable memory with optional topic, draft flag or explicit branch state, image input, and artifact refs |
| `search_context` | Query → embedding → Firestore vector similarity search (cosine, top-K) with metadata filters |
| `fetch_context` | Retrieve one stored memory by document ID after search |
| `deprecate_context` | Soft-delete: mark document as deprecated, record superseding document ID |
| `deprecate_context` | Soft-delete: mark document as deprecated, record superseding document ID, and record supersession_reason ("changed" sets valid_until, "corrected" does not) |
| `consolidate_context` | Merge N related memories into one canonical active memory via LLM; deprecates all sources with `superseded_by` pointing to the merged result. Defaults to WIP queue for a topic; accepts explicit `source_ids` for targeted consolidation |

### Key Source Files (all under `functions/src/`)
Expand All @@ -95,11 +95,11 @@ Auth uses timing-safe token comparison. Origin allowlisting supports `"*"` wildc

**remember_context**: Chat/admin input → server defaults/inference for metadata and lifecycle state → Gemini multimodal normalization (if image) → canonical `content` + internal `retrieval_text` → Gemini embedding (deployment currently pinned to 768-dim) → Firestore document with vector + metadata

**search_context**: Query text → Gemini embedding → Firestore `findNearest()` (cosine distance, top-K) with required `branch_state` and optional topic filter
**search_context**: Query text → Gemini embedding → Firestore `findNearest()` (cosine distance, top-K) with required `branch_state` and optional topic filter; optional `valid_at` post-filters results in the service layer by temporal validity window

**fetch_context**: Document ID → direct Firestore read of one stored memory

**deprecate_context**: Document ID + superseding ID → update `branch_state` to "deprecated", set `superseded_by`
**deprecate_context**: Document ID + superseding ID → update `branch_state` to "deprecated", set `superseded_by`; `supersession_reason` ("changed" default sets `valid_until` to now, "corrected" does not) and optional `initiator` are recorded on the deprecated document

**consolidate_context**: Topic (default WIP queue) or explicit `source_ids` → gather source memories → Gemini merge of N contents into one → store merged result as `active` → deprecate every source with `superseded_by` pointing to the merged ID

Expand Down
27 changes: 22 additions & 5 deletions functions/src/mcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from "./service.js";
import {
BRANCH_STATES,
SUPERSESSION_REASONS,
type BranchState,
type McpToolName
} from "./types.js";
Expand Down Expand Up @@ -281,7 +282,11 @@ export function createMetaCortexMcpServer(
.min(1)
.max(20)
.optional()
.describe("Max results to return. Defaults to 5.")
.describe("Max results to return. Defaults to 5."),
valid_at: z
.number()
.optional()
.describe("Optional epoch-ms timestamp. When provided, only returns memories valid at that point in time (valid_from <= valid_at < valid_until, excluding corrected records).")
}
},
async args => {
Expand All @@ -293,7 +298,8 @@ export function createMetaCortexMcpServer(
query_length: args.query.trim().length,
filter_topic: normalizedFilterTopic,
filter_state: requestedFilterState,
limit: args.limit
limit: args.limit,
valid_at: args.valid_at
};
const result = await observeToolCall(
"search_context",
Expand Down Expand Up @@ -424,13 +430,23 @@ export function createMetaCortexMcpServer(
superseding_id: z
.string()
.min(1)
.describe("The id of the new memory that replaces it.")
.describe("The id of the new memory that replaces it."),
supersession_reason: z
.enum(SUPERSESSION_REASONS)
.optional()
.describe("Why the memory is being superseded. 'changed' (default) means the old fact was true of its era, records valid_until. 'corrected' means the old record was never true, excluded from valid-time results but kept for audit."),
initiator: z
.enum(["user", "agent"])
.optional()
.describe("Who initiated this deprecation, for audit purposes.")
}
},
async args => {
const requestSummary = {
id: args.id,
superseding_id: args.superseding_id
superseding_id: args.superseding_id,
supersession_reason: args.supersession_reason,
initiator: args.initiator
};
const result = await observeToolCall(
"deprecate_context",
Expand All @@ -439,7 +455,8 @@ export function createMetaCortexMcpServer(
record => ({
id: record.id,
superseding_id: record.superseding_id,
previous_state: record.previous_state
previous_state: record.previous_state,
supersession_reason: record.supersession_reason
})
);

Expand Down
32 changes: 26 additions & 6 deletions functions/src/memoryRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import type {
BranchState,
MemoryDocument,
MemoryMedia,
MemoryMetadata
MemoryMetadata,
SupersessionReason
} from "./types.js";

export interface StoreMemoryParams {
Expand All @@ -28,7 +29,11 @@ export interface MemoryRepository {
store(params: StoreMemoryParams): Promise<{ document: MemoryDocument; created: boolean }>;
search(params: SearchMemoryParams): Promise<MemoryDocument[]>;
get(documentId: string): Promise<MemoryDocument | null>;
deprecate(documentId: string, supersedingDocumentId: string): Promise<{ previousState: BranchState }>;
deprecate(
documentId: string,
supersedingDocumentId: string,
options?: { supersessionReason?: SupersessionReason; initiator?: "user" | "agent" }
): Promise<{ previousState: BranchState }>;
getConsolidationQueue(moduleName?: string): Promise<MemoryDocument[]>;
}

Expand Down Expand Up @@ -162,7 +167,8 @@ export class FirestoreMemoryRepository implements MemoryRepository {

async deprecate(
documentId: string,
supersedingDocumentId: string
supersedingDocumentId: string,
options?: { supersessionReason?: SupersessionReason; initiator?: "user" | "agent" }
): Promise<{ previousState: BranchState }> {
const docRef = this.firestore
.collection(this.collectionName)
Expand All @@ -177,11 +183,25 @@ export class FirestoreMemoryRepository implements MemoryRepository {
const data = snapshot.data() as FirestoreMemoryDocument;
const previousState = data.metadata.branch_state;

await docRef.update({
const now = Date.now();
const resolvedReason = options?.supersessionReason ?? "changed";

const updates: Record<string, unknown> = {
"metadata.branch_state": "deprecated",
"metadata.superseded_by": supersedingDocumentId,
"metadata.updated_at": Date.now()
});
"metadata.updated_at": now,
"metadata.supersession_reason": resolvedReason
};

if (resolvedReason === "changed") {
updates["metadata.valid_until"] = now;
}

if (options?.initiator) {
updates["metadata.initiator"] = options.initiator;
}

await docRef.update(updates);

return { previousState };
}
Expand Down
41 changes: 35 additions & 6 deletions functions/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import type {
FetchContextInput,
FetchContextResult,
MemoryDocument,
MemoryMetadata,
RememberContextInput,
SearchContextInput,
SearchContextResult,
StoreContextInput,
StoreContextResult
StoreContextResult,
SupersessionReason
} from "./types.js";
import type { MemoryRepository } from "./memoryRepository.js";

Expand Down Expand Up @@ -62,6 +64,12 @@ export class MetaCortexService {
modality: preparedContent.modality,
...(normalizedArtifactRefs.length > 0
? { artifact_refs: normalizedArtifactRefs }
: {}),
...(typeof input.valid_from === "number"
? { valid_from: input.valid_from }
: {}),
...(typeof input.valid_until === "number"
? { valid_until: input.valid_until }
: {})
} as const;

Expand Down Expand Up @@ -101,7 +109,9 @@ export class MetaCortexService {
branch_state: branchState,
artifact_refs: input.artifact_refs,
image_base64: normalizeOptionalText(input.image_base64),
image_mime_type: normalizeOptionalText(input.image_mime_type)
image_mime_type: normalizeOptionalText(input.image_mime_type),
valid_from: input.valid_from,
valid_until: input.valid_until
});
}

Expand All @@ -113,13 +123,17 @@ export class MetaCortexService {
text: normalizedQuery,
taskType: "RETRIEVAL_QUERY"
});
const matches = await this.repository.search({
let matches = await this.repository.search({
queryVector,
limit: input.limit ?? this.config.topK,
filterModule: filterTopic,
filterState
});

if (typeof input.valid_at === "number") {
matches = matches.filter(match => matchesValidAt(match.metadata, input.valid_at!));
}

return {
matches,
appliedFilters: {
Expand All @@ -141,15 +155,21 @@ export class MetaCortexService {
}

async deprecateContext(input: DeprecateContextInput): Promise<DeprecateContextResult> {
const resolvedReason: SupersessionReason = input.supersession_reason ?? "changed";
const { previousState } = await this.repository.deprecate(
input.id,
input.superseding_id
input.superseding_id,
{
supersessionReason: resolvedReason,
initiator: input.initiator
}
);

return {
id: input.id,
superseding_id: input.superseding_id,
previous_state: previousState
previous_state: previousState,
supersession_reason: resolvedReason
};
}

Expand Down Expand Up @@ -300,7 +320,8 @@ export function buildDeprecatePayload(
item: {
id: result.id,
branch_state: "deprecated",
superseded_by: result.superseding_id
superseded_by: result.superseding_id,
supersession_reason: result.supersession_reason
},
previous_state: result.previous_state
};
Expand Down Expand Up @@ -406,3 +427,11 @@ function normalizePublicModality(value: string): "text" | "image" | "mixed" {

return "text";
}

function matchesValidAt(metadata: MemoryMetadata, validAt: number): boolean {
const validFromOk = typeof metadata.valid_from === "undefined" || metadata.valid_from <= validAt;
const validUntilOk = typeof metadata.valid_until === "undefined" || metadata.valid_until > validAt;
const reasonOk = metadata.supersession_reason !== "corrected";

return validFromOk && validUntilOk && reasonOk;
}
15 changes: 15 additions & 0 deletions functions/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ export const BRANCH_STATES = [
"wip"
] as const;

export const SUPERSESSION_REASONS = ["changed", "corrected"] as const;

export const MEMORY_MODALITIES = [
"text",
"image",
"mixed"
] as const;

export type BranchState = (typeof BRANCH_STATES)[number];
export type SupersessionReason = (typeof SUPERSESSION_REASONS)[number];
export type MemoryModality = (typeof MEMORY_MODALITIES)[number];
export type McpToolName = (typeof MCP_TOOL_NAMES)[number];

Expand All @@ -36,6 +39,10 @@ export interface MemoryMetadata {
modality: MemoryModality;
artifact_refs?: string[];
superseded_by?: string;
valid_from?: number;
valid_until?: number;
supersession_reason?: SupersessionReason;
initiator?: "user" | "agent";
}

export interface MemoryDocument {
Expand All @@ -54,13 +61,16 @@ export interface StoreContextInput {
artifact_refs?: string[];
image_base64?: string;
image_mime_type?: string;
valid_from?: number;
valid_until?: number;
}

export interface SearchContextInput {
query: string;
filter_topic?: string;
filter_state?: BranchState;
limit?: number;
valid_at?: number;
}

export interface RememberContextInput {
Expand All @@ -71,6 +81,8 @@ export interface RememberContextInput {
artifact_refs?: string[];
image_base64?: string;
image_mime_type?: string;
valid_from?: number;
valid_until?: number;
}

export interface StoreContextResult {
Expand Down Expand Up @@ -102,12 +114,15 @@ export interface FetchContextResult {
export interface DeprecateContextInput {
id: string;
superseding_id: string;
supersession_reason?: SupersessionReason;
initiator?: "user" | "agent";
}

export interface DeprecateContextResult {
id: string;
superseding_id: string;
previous_state: BranchState;
supersession_reason: SupersessionReason;
}

export interface ConsolidationQueueInput {
Expand Down
Loading
Loading