feat(search): add temporal before and after filters - #206
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces strict temporal before and after search filters end-to-end (protocol → server → database), and wires them through the CLI, MCP tools, and web UI, with updated documentation and boundary-focused tests.
Changes:
- Add
temporal.before/temporal.afterto the wire protocol and map them into space search parameters. - Implement strict cutoff semantics in SQL using PostgreSQL range operators (
<</>>) against a closed point range. - Expose the new filters in CLI + MCP + web advanced search UI, and add integration/unit tests plus docs updates.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/integration-test.ts | Adds CLI integration coverage for --temporal-before/--temporal-after. |
| packages/web/src/store/filter.ts | Extends web filter state + param mapping to support before/after. |
| packages/web/src/store/filter.test.ts | Adds unit tests for web param conversion and chip rendering for before/after. |
| packages/web/src/lib/url-state.test.ts | Verifies URL encode/decode round-trip with single-point temporal modes. |
| packages/web/src/components/search/AdvancedSearchPanel.tsx | Adds before/after options to the advanced search UI and disables end timestamp appropriately. |
| packages/server/rpc/memory/memory.ts | Maps wire temporal filters to engine/db temporal parameters (now including before/after). |
| packages/server/rpc/memory/memory.integration.test.ts | Adds integration test asserting strict cutoff semantics for before/after. |
| packages/protocol/memory.test.ts | Adds protocol tests for accepting offset timestamps for before/after. |
| packages/protocol/fields.ts | Extends temporal filter schema with before/after. |
| packages/database/space/migrate/idempotent/002_search.sql | Changes temporal before/after SQL filtering to strict range comparisons. |
| packages/cli/mcp/server.ts | Exposes before/after in MCP tool schemas and forwards them into RPC search params. |
| packages/cli/mcp/server.test.ts | Adds MCP forwarding tests for before/after in search and export tools. |
| packages/cli/commands/memory.ts | Adds CLI flags for --temporal-before/--temporal-after in search/export and forwards them to API. |
| docs/typescript-client.md | Documents before/after usage in the TypeScript client example. |
| docs/search.md | Updates search filter docs to mention --temporal-before/--temporal-after. |
| docs/mcp/me_memory_search.md | Updates MCP search tool docs to include before/after and strictness semantics. |
| docs/mcp/me_memory_export.md | Updates MCP export tool docs to include before/after. |
| docs/concepts.md | Expands conceptual temporal query modes to include before/after and strict boundary explanation. |
| docs/cli/me-memory.md | Documents new CLI flags for me memory search and me memory export. |
Suppressed comments (5)
packages/protocol/fields.ts:159
- temporalFilterSchema allows multiple modes (e.g., {before, after}) even though the API treats them as mutually exclusive; this can lead to inconsistent precedence across clients/servers. Add a schema refinement so at most one temporal mode is provided.
export const temporalFilterSchema = z.object({
before: timestampSchema.optional(),
after: timestampSchema.optional(),
contains: timestampSchema.optional(),
overlaps: z
.object({
start: timestampSchema,
end: timestampSchema,
})
.optional(),
within: z
.object({
start: timestampSchema,
end: timestampSchema,
})
.optional(),
});
packages/cli/commands/memory.ts:458
- The CLI accepts multiple --temporal-* flags at once but silently picks the first match in the else-if chain. Since the temporal modes are mutually exclusive, this should fail fast with a clear error when more than one flag is provided.
This issue also appears on line 1040 of the same file.
// Validate at least one search criterion
if (
!semantic &&
!fulltext &&
!opts.grep &&
!tree &&
!meta &&
!opts.temporalBefore &&
!opts.temporalAfter &&
!opts.temporalContains &&
!opts.temporalOverlaps &&
!opts.temporalWithin
) {
const msg =
"At least one search criterion required (query, --semantic, --fulltext, --grep, --tree, --meta, or --temporal-*).";
if (fmt === "text") {
clack.log.error(msg);
} else {
output({ error: msg }, fmt, () => {});
}
process.exit(1);
}
// Build temporal filter
let temporal: Record<string, unknown> | null = null;
if (opts.temporalBefore) {
temporal = { before: opts.temporalBefore };
} else if (opts.temporalAfter) {
temporal = { after: opts.temporalAfter };
} else if (opts.temporalContains) {
temporal = { contains: opts.temporalContains };
} else if (opts.temporalOverlaps) {
const parts = opts.temporalOverlaps
.split(",")
.map((s: string) => s.trim());
if (parts.length !== 2 || !parts[0] || !parts[1]) {
handleError(new Error("--temporal-overlaps requires start,end"), fmt);
}
temporal = { overlaps: { start: parts[0], end: parts[1] } };
} else if (opts.temporalWithin) {
const parts = opts.temporalWithin
.split(",")
.map((s: string) => s.trim());
if (parts.length !== 2 || !parts[0] || !parts[1]) {
handleError(new Error("--temporal-within requires start,end"), fmt);
}
temporal = { within: { start: parts[0], end: parts[1] } };
}
packages/cli/commands/memory.ts:1067
- Like
me memory search,me memory exportcurrently allows multiple --temporal-* flags but silently chooses one via else-if. Since these modes are mutually exclusive, reject conflicting combinations so exports are deterministic and user intent is clear.
// Build temporal filter
if (opts.temporalBefore) {
searchParams.temporal = { before: opts.temporalBefore };
} else if (opts.temporalAfter) {
searchParams.temporal = { after: opts.temporalAfter };
} else if (opts.temporalContains) {
searchParams.temporal = { contains: opts.temporalContains };
} else if (opts.temporalOverlaps) {
const parts = opts.temporalOverlaps
.split(",")
.map((s: string) => s.trim());
if (parts.length !== 2 || !parts[0] || !parts[1]) {
handleError(new Error("--temporal-overlaps requires start,end"), fmt);
}
searchParams.temporal = {
overlaps: { start: parts[0], end: parts[1] },
};
} else if (opts.temporalWithin) {
const parts = opts.temporalWithin
.split(",")
.map((s: string) => s.trim());
if (parts.length !== 2 || !parts[0] || !parts[1]) {
handleError(new Error("--temporal-within requires start,end"), fmt);
}
searchParams.temporal = {
within: { start: parts[0], end: parts[1] },
};
}
packages/cli/mcp/server.ts:381
- The MCP tool schema for temporal filters allows multiple modes (before/after/contains/overlaps/within) at the same time, but the API treats them as mutually exclusive. Adding a refinement here produces a clearer, earlier tool error instead of relying on downstream behavior.
This issue also appears on line 1229 of the same file.
temporal: z
.object({
before: z
.string()
.optional()
.nullable()
.describe("Find memories strictly before this point in time"),
after: z
.string()
.optional()
.nullable()
.describe("Find memories strictly after this point in time"),
contains: z
.string()
.optional()
.nullable()
.describe("Find memories containing this point in time"),
overlaps: z
.object({
start: z.string().describe("Start of range"),
end: z.string().describe("End of range"),
})
.optional()
.nullable()
.describe("Find memories overlapping this range"),
within: z
.object({
start: z.string().describe("Start of range"),
end: z.string().describe("End of range"),
})
.optional()
.nullable()
.describe("Find memories fully within this range"),
})
.optional()
.nullable()
.describe("Temporal filter for search"),
packages/cli/mcp/server.ts:1265
me_memory_export's MCP input schema also permits multiple temporal modes simultaneously even though they're mutually exclusive. Adding the same refinement as search avoids ambiguous exports and gives a clear tool-level validation error.
temporal: z
.object({
before: z
.string()
.optional()
.nullable()
.describe("Find memories strictly before this point in time"),
after: z
.string()
.optional()
.nullable()
.describe("Find memories strictly after this point in time"),
contains: z
.string()
.optional()
.nullable()
.describe("Find memories containing this point in time"),
overlaps: z
.object({
start: z.string().describe("Start of range"),
end: z.string().describe("End of range"),
})
.optional()
.nullable()
.describe("Find memories overlapping this range"),
within: z
.object({
start: z.string().describe("Start of range"),
end: z.string().describe("End of range"),
})
.optional()
.nullable()
.describe("Find memories fully within this range"),
})
.optional()
.nullable()
.describe("Temporal filter"),
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
beforeandafterfilters across the API, CLI, MCP tools, exports, and web UI<<and>>against a closed point rangeTesting
./bun run check:fullLinear: TNT-256