Skip to content

Content chunking

A chunk is one unit of semantic recall. When you query, Jennah ranks your agent's chunks by cosine distance and returns the nearest matches. How content is split into chunks directly affects recall precision and embedding limits.

The embedding input limit

When you commit a chunk with rawContent and no embedding, Jennah generates the vector for you with an embedding model that accepts about 2048 tokens of input.

If content exceeds this limit, the model embeds a prefix and stops. rawContent is stored and returned in full on a match, but the vector only represents the prefix up to the token limit. Text past the limit is not indexed in vector space.

Truncation reporting

Jennah reports truncation on commit receipts. Inspect commit receipts to verify that long inputs fit within embedding token limits.

Reading the truncation report

Every memory:commit receipt names the chunks whose embedding was truncated:

{
  "commitTimestamp": "2026-07-30T09:12:44.113Z",
  "vectorRows": "1",
  "truncatedChunkIds": ["daily-2026-07-30-slack"]
}

An empty or absent truncatedChunkIds list means every chunk in that commit was embedded in full. Truncation is reported without failing the commit.

To fail the commit rather than storing a truncated embedding, set rejectOnTruncation on the request:

{
  "vectors": [{ "chunkId": "doc-42", "rawContent": "...long..." }],
  "rejectOnTruncation": true
}

The entire commit is then rejected with FAILED_PRECONDITION and nothing is written.

To audit chunks that are already stored, use memory:inspect (see Reading a workspace back for paging a listing to the end):

{
  "vectors": {
    "chunks": [{
      "chunkId": "daily-2026-07-30-slack",
      "rawContent": "...",
      "truncated": true,
      "tokenCount": "9001"
    }]
  }
}

Unmeasured chunk truncation

Some chunks omit the truncated field. An absent field indicates unmeasured truncation state rather than false. Re-committing the chunkId evaluates and reports truncation status.

Sizing a split

tokenCount is the token count of submitted content:

pieces needed ≈ tokenCount / 2048

A chunk reporting tokenCount: 9001 needs roughly five pieces.

Character counts vs. tokens

Characters are an unreliable proxy for tokens. English averages roughly 4 characters per token (~8000 characters per chunk limit), whereas Japanese, Chinese, and Korean average close to 1 token per character (~2000 characters per chunk limit). Inspect tokenCount rather than relying on character length.

Chunk granularity and retrieval

Staying under token limits is required, but smaller chunks also improve retrieval precision:

  • Split on meaning, not size. One chunk per section, topic, or exchange. A fixed-width window may split semantic context across chunks.
  • Distill before storing. Summarize lengthy conversations or logs before committing to prevent low-value chunks from occupying query result slots.
  • Keep pointers to large sources. Store concise summaries with external IDs or URIs to fetch raw content as needed.
  • Retire chunks that stop being true. A stale chunk competes for the same result slots as the one that replaced it. See Temporal memory.

Structuring metadata vs. raw content

Including inline provenance tags (such as [#eng-billing 2026-07-30]) in rawContent dilutes embedding similarity and prevents exact metadata filtering.

Use metadata instead. It is stored with the chunk and returned with every hit, but it is not embedded - only rawContent is:

{
  "vectors": [{
    "chunkId": "daily-2026-07-30-slack",
    "rawContent": "Alice fixed duplicate invoice lines in billing.",
    "metadata": {
      "employee": "alice",
      "day": "2026-07-30",
      "source": "slack"
    }
  }]
}

Then scope recall to a subset with filters:

{
  "semantic": {
    "queryText": "what did alice ship recently?",
    "limit": 8,
    "filters": [
      { "key": "employee", "value": "alice" },
      { "key": "source", "value": "slack" }
    ]
  }
}

Filters combine with AND and are applied before ranking, so you get the nearest limit chunks from among those that match - not a filter applied to an already-ranked result set.

For complete metadata specifications and filter rules, see Metadata.

Automated chunking

While direct commits require callers to chunk content explicitly, memory:form automates chunking for conversational turns by extracting facts and writing each under a content-derived ID.

Use direct commits (memory:commit) when:

  • Natural boundaries exist: Structured documents, section-based articles, or row-based records have clear structural boundaries.
  • Low write latency is required: Direct commits execute in milliseconds, whereas formation takes seconds due to model evaluation.
  • Explicit ID control is needed: Formation derives IDs from content hashes, whereas direct commits allow caller-specified chunk IDs.
  • Exact content preservation is required: Formation extracts and summarizes salient facts; direct commits store payloads verbatim.
  • Large tables or structured data: Direct commits allow fine-grained chunking per row or section rather than relying on prose extraction (Tables, code, and other structured content).

Use formation (memory:form) for unstructured conversations where semantic boundaries must be inferred.

Field names and types

Responses use lowerCamelCase (truncatedChunkIds, tokenCount, rawContent), while requests accept either camelCase or snake_case. The API Reference lists fields in snake_case.

64-bit integers are encoded as JSON strings (such as "tokenCount": "9001" and "vectorRows": "1").