Internal Workings of Request Tracking and Logging in ThePrimeagen/99

ThePrimeagen/99 tracks every AI-driven editor operation through a centralized state object that assigns unique request IDs, caches per-request logs in JSON format, and provides lifecycle hooks for cancellation and UI updates.

ThePrimeagen/99 is a Neovim plugin that orchestrates AI-driven editor operations through a robust request tracking and logging system. Understanding the internal workings of request tracking and logging reveals how the plugin maintains state consistency, enables request cancellation, and provides detailed debugging capabilities through per-request log isolation.

Request Lifecycle: From Context to Completion

Every AI operation in 99 follows a strict lifecycle managed by three core components: RequestContext, Request, and the provider layer.

Context Creation and Unique ID Assignment

When a user triggers an operation like search or tutorial, RequestContext.from_current_buffer in lua/99/request-context.lua constructs a context object containing:

  • A unique xid (request ID)
  • Buffer information and temporary file paths
  • A scoped logger instance via Logger:set_id(xid)
-- From lua/99/request-context.lua
local context = RequestContext.from_current_buffer("search")
-- context.xid is now a unique identifier
-- context.logger is scoped to this specific request

Request Initialization and State Management

The Request.new(context) constructor in lua/99/request/init.lua creates a request object with initial state "ready" and scopes the logger to the "Request" area using context.logger:set_area("Request").

Starting the Request and Provider Execution

When Request:start(observer) is called, it:

  1. Validates the request state
  2. Finalizes the context (adds markdown files, range info, temp-file location)
  3. Builds and saves the prompt
  4. Forwards the request to the selected provider

The provider layer in lua/99/providers.lua handles BaseProvider:make_request, which spawns a system process and streams stdout/stderr back through the observer.

Completion and State Tracking

When the provider finishes, observer.on_complete triggers context._99:finish_request(context, status) in lua/99/init.lua. This updates the request status to "success", "failed", or "cancelled" and stores operation-specific data like tutorials or search results.

Cancellation and Cleanup

The Request:cancel() method aborts running system processes and sets state to "cancelled", with the logger recording the cancellation event.

Centralized Request Tracking with _99_State

All active and historic requests are stored in the singleton _99_State object initialized during plugin setup.

Tracking New Requests

The track_request function in lua/99/init.lua creates a structured entry for every request:

function _99_State:track_request(context)
  assert(context.operation, "must have an operation defined to track the request")
  local point = context.range and context.range.start or Point:from_cursor()
  local entry = {
    context = context,
    status  = "requesting",
    point   = point,
    started_at = time.now(),
    operation_data = nil,
  }
  table.insert(self.__request_history, entry)
  self.__request_by_id[context.xid] = entry
  return entry
end

This maintains two data structures:

  • __request_history: Ordered list for UI components like the quick-fix list
  • __request_by_id: Map keyed by xid for fast lookup and log association

Finishing and Storing Results

The finish_request method updates the entry status and routes successful results to type-specific storage:

function _99_State:finish_request(context, status)
  local id = context.xid
  local entry = self.__request_by_id[id]
  if not entry then return end
  entry.status = status
  local data = entry.operation_data
  if entry.status == "success" and data then
    if data.type == "tutorial" then
      table.insert(self.__tutorials, data)
    elseif data.type == "search" then
      table.insert(self.__searches, data)
    end
  end
end

Querying Active and Historic Requests

The state object provides several APIs for UI interaction:

  • active_request_count(): Returns the number of requests with status == "requesting" for the throbber UI
  • stop_all_requests(): Iterates over __request_by_id and calls context:stop() on each in-flight request
  • previous_requests_to_qfix(): Transforms __request_history into quick-fix items for :copen
  • clear_previous_requests(): Removes non-active entries and purges associated logs

Per-Request Logging Architecture

The logging system isolates logs by request ID using a lightweight Logger module with configurable sinks.

Logger Sinks and Configuration

Three sink implementations control log output:

  • VoidSink: Discards logs (default)
  • PrintSink: Calls print() for each line
  • FileSink: Writes JSON lines to a file using vim.uv.fs_open

Configuration happens through Logger:configure:

require("99").setup({
  logger = {
    level = require("99.logger.level").DEBUG,
    type  = "file",
    path  = vim.fn.stdpath("cache") .. "/99.log",
    max_requests_cached = 10,
  },
})

Request Scoping and Log Caching

Each request receives a scoped logger instance:

-- In RequestContext creation
logger = Logger:set_id(xid)          -- Adds request ID for caching
logger = logger:set_area("Request")  -- Adds Area meta-field

The set_id method is critical because _cache_log stores each line in logger_cache[id]. The cache automatically trims to max_requests_in_logger_cache (default 5) to prevent memory bloat.

All log calls flow through _log, which:

  1. Validates the logger's level
  2. Builds a table with level, msg, and key/value pairs
  3. Merges extra_params (including id and Area)
  4. Encodes to JSON and writes to the sink
  5. Caches the JSON string under the request ID

Viewing Cached Logs

The plugin exposes functions to inspect logs:

_99.view_logs()          -- Opens most recent cached logs full-screen
_99.next_request_logs()  -- Scroll forward through cached sets
_99.prev_request_logs()  -- Scroll backward through cached sets

These functions pull JSON-encoded logs from Logger.logs() and display them using the Window helper.

Practical Code Examples

Initiating a Search Request

-- In your Neovim configuration
require("99").setup({ show_in_flight_requests = true })

-- Trigger a search operation
require("99").search()

This invokes search (line 50 of lua/99/init.lua), which creates a context with operation = "search", tracks it via _99_State:track_request, and delegates to ops.search which ultimately calls Request:start().

Cancelling All In-Flight Requests

require("99").stop_all_requests()

This calls stop_all_requests (lines 42-48 of lua/99/init.lua), which iterates through all entries in __request_by_id and invokes each entry's context:stop(). The context's stop method runs cleanup callbacks and the request's cancel method kills the underlying system process.

Accessing Per-Request Logs

-- Show the most recent request logs in a full-screen buffer
require("99").view_logs()

-- Cycle through cached logs
require("99").next_request_logs()
require("99").prev_request_logs()

These functions retrieve JSON-encoded logs from the per-request cache and display them using the internal Window helper.

Custom Logger Configuration

require("99").setup({
  logger = {
    level = require("99.logger.level").DEBUG,
    type  = "file",
    path  = vim.fn.stdpath("cache") .. "/99.log",
    max_requests_cached = 10,
  },
})

Logger:configure (lines 89-115 of lua/99/logger/logger.lua) switches the sink to FileSink, sets the threshold level, and adjusts the cache limit to retain logs for the last 10 requests instead of the default 5.

Key Source Files

File Role Direct Link
lua/99/request-context.lua Builds per-buffer context, assigns unique logger (Logger:set_id). request-context.lua
lua/99/request/init.lua Implements the Request object (state machine, start, cancel, prompt handling). request/init.lua
lua/99/init.lua Central state (_99_State), request tracking (track_request, finish_request), UI helpers, public API. init.lua
lua/99/logger/logger.lua Core logging implementation, sinks, per-request caching, configuration. logger/logger.lua
lua/99/providers.lua Provider abstraction that runs external AI commands and reports via observer. providers.lua
lua/99/ops/*.lua High-level operations (search, tutorial, etc.) that orchestrate request creation. ops/search.lua

Summary

  • Unique request IDs (xid) bind together loggers, contexts, and cache entries, enabling isolated per-request debugging and safe concurrent handling of multiple AI operations.
  • The central _99_State object provides a single source of truth for request metadata, making UI components like the throbber and quick-fix list trivial to implement without direct provider coupling.
  • The logger cache guarantees that recent logs remain accessible even after the original request object is garbage collected, while max_requests_cached protects memory through automatic LRU-style trimming.
  • All logging is JSON-encoded, simplifying downstream processing or external log aggregation without requiring custom parsers.
  • Structured lifecycle hooks (track, finish, cancel) ensure that every AI operation integrates automatically with state management and logging without additional boilerplate in operation-specific code.

Frequently Asked Questions

How does 99 assign unique identifiers to each request?

The plugin generates a unique xid during context creation in lua/99/request-context.lua. This identifier is immediately passed to Logger:set_id(xid) to scope all subsequent logs to this specific request, and is used as the key in __request_by_id for state tracking. The xid binds together the context, logger, and state entry throughout the request lifecycle.

What happens to logs after a request completes?

Logs are cached in memory under the request's xid with a default limit of 5 requests (max_requests_in_logger_cache). The Logger module automatically trims old entries when this limit is exceeded to prevent memory bloat. If configured with a FileSink, logs are also persisted to disk as JSON lines before the in-memory cache is cleared, ensuring durable records even after the request object is garbage collected.

Can I cancel a running AI request in 99?

Yes. The Request:cancel() method in lua/99/request/init.lua aborts the underlying system process and updates the state to "cancelled", with the logger recording the cancellation event. You can cancel all in-flight requests at once using require("99").stop_all_requests(), which iterates through __request_by_id and invokes the stop callback for each active entry.

Where are request statuses stored and how does the UI access them?

Request statuses are stored in the _99_State singleton within lua/99/init.lua. The state maintains __request_history (an ordered list for the quick-fix window) and __request_by_id (a map for fast lookup). UI components query active_request_count() to display the throbber and call previous_requests_to_qfix() to populate the quick-fix list with historic request data, including file locations and status information.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →