Skip to content

AUDIT-11 — Medium — Session truncation is never wired; when enabled it orphans tool messages #68

Closed

Description

@getdownandcode

Summary

Severity: Medium
Area: Sessions
Affected files: agentkit/api/deps.py:86-92, agentkit/memory/redis_store.py:37-52
Suggested branch: fix/session-history-truncation
Suggested PR title: fix(memory): wire bounded session history with boundary-safe truncation

This is finding AUDIT-11 of 18 from a consolidated audit of agentkit/. It is deliberately self-contained so it can be fixed on its own branch with its own PR.

Related findings: Related: AUDIT-10.


AUDIT-11 — Medium — Session truncation is never wired; when enabled it orphans tool messages

Files

  • agentkit/api/deps.py:86-92
  • agentkit/memory/redis_store.py:37-52

Current code — agentkit/api/deps.py:86-92

    settings: Settings = getattr(request.app.state, "settings", None) or get_current_settings(
        request
    )

    run_store = PostgresMemoryStore(factory)
    session_store = RedisMemoryStore(redis_client, ttl_s=settings.SESSION_TTL_S)
    return TieredMemoryStore(session_store=session_store, run_store=run_store)

Current code — agentkit/memory/redis_store.py:37-52

    @staticmethod
    def truncate_messages(messages: list[Message], max_messages: int) -> list[Message]:
        """Truncate message history, preserving the leading system prompt and recent messages."""
        if max_messages <= 0 or len(messages) <= max_messages:
            return messages

        if messages and messages[0].role == Role.SYSTEM:
            system_msg = messages[0]
            non_system = messages[1:]
            # The system prompt occupies one of the max_messages slots, so only
            # max_messages - 1 conversational turns fit. Taking a full tail would silently
            # return max_messages + 1 entries and grow the history without bound.
            budget = max_messages - 1
            return [system_msg, *non_system[-budget:]] if budget > 0 else [system_msg]

        return messages[-max_messages:]

Why this is a bug

  1. max_messages is never supplied (no setting exists for it) → session histories grow without bound in Redis.
  2. The truncation is also incorrect when enabled: a naive tail cut can start the history with a tool message whose assistant tool_calls turn was dropped, producing a history the providers reject (OpenAI: "messages with role 'tool' must be a response to a preceding message with 'tool_calls'").

Reproduced

history: [system, user, assistant(c1), tool(c1), assistant(c2), tool(c2)]
truncate(max_messages=4) -> [system, tool(c1), assistant(c2), tool(c2)]   # tool(c1) is orphaned
truncate(max_messages=3) -> [system, assistant(c2), tool(c2)]            # ok

Pass cases

  • A setting (e.g. SESSION_MAX_MESSAGES) exists, is documented in .env.example / docs/SPEC.md, and is actually passed to RedisMemoryStore.
  • Truncation never leaves a tool message without its matching assistant tool_calls turn, and never leaves an assistant tool_calls turn without its results.
  • The system prompt is preserved and the total returned message count never exceeds max_messages.
  • A regression test truncating a history containing tool calls at every possible cut point asserts the result is a provider-valid history.
  • A test asserts a long session does not grow the stored history beyond the configured bound.


Definition of done (per AGENTS.md)

  • New/updated unit and integration tests covering the pass cases above, all passing (pytest).
  • ruff check ., ruff format --check . and mypy agentkit clean.
  • Docstrings updated where behaviour changed; docs/PROGRESS.md updated (status, PR link, commit count, notes).
  • A new ADR in docs/DECISIONS.md if an architectural decision was made.
  • Branch pushed and PR opened (never commit directly to main).

Baseline at the time of writing: pytest is green on main (all tests pass; tests/real/* skipped unless AGENTKIT_REAL_INFRA=1).

Activity

  1. getdownandcode commented on Oct 20, 2026

    @getdownandcode
    OwnerAuthor

    Audit index (18 separate issues, fix one by one)

    ID Issue Severity
    AUDIT-01 #58 Critical
    AUDIT-02 #59 Critical
    AUDIT-03 #60 High
    AUDIT-04 #61 High
    AUDIT-05 #62 High
    AUDIT-06 #63 High/Medium
    AUDIT-07 #64 Medium-High
    AUDIT-08 #65 Medium-High
    AUDIT-09 #66 Medium-High
    AUDIT-10 #67 Medium-High
    AUDIT-11 #68 Medium
    AUDIT-12 #69 Medium
    AUDIT-13 #70 Medium
    AUDIT-14 #71 Medium
    AUDIT-15 #72 Medium
    AUDIT-16 #73 Medium
    AUDIT-17 #74 Low
    AUDIT-18 #75 Low/Deployment

    Each issue is self-contained: current code, reproduction, pass cases, suggested branch and PR title. Reference the relevant one with Fixes #NN in your PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions