Skip to content

feat(search): add temporal before and after filters - #206

Merged
jgpruitt merged 1 commit into
mainfrom
jgpruitt/before-and-after
Aug 9, 2026
Merged

jgpruitt merged 1 commit into
mainfrom
jgpruitt/before-and-after

Conversation

@jgpruitt

@jgpruitt jgpruitt commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add strict temporal before and after filters across the API, CLI, MCP tools, exports, and web UI
  • implement database filtering with PostgreSQL << and >> against a closed point range
  • document TTL usage and cover cutoff boundaries, forwarding, and UI state

Testing

  • ./bun run check:full
  • 1,617 package tests passed
  • 32 end-to-end tests passed

Linear: TNT-256

Copilot AI lite review requested due to automatic review settings August 9, 2026 16:15
@jgpruitt jgpruitt self-assigned this Aug 9, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.after to 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 export currently 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.

@jgpruitt
jgpruitt merged commit 7d5c45a into main Aug 9, 2026
7 checks passed
@jgpruitt
jgpruitt deleted the jgpruitt/before-and-after branch August 9, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants