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:
- Validates the request state
- Finalizes the context (adds markdown files, range info, temp-file location)
- Builds and saves the prompt
- 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 byxidfor 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 withstatus == "requesting"for the throbber UIstop_all_requests(): Iterates over__request_by_idand callscontext:stop()on each in-flight requestprevious_requests_to_qfix(): Transforms__request_historyinto quick-fix items for:copenclear_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:
- Validates the logger's level
- Builds a table with
level,msg, and key/value pairs - Merges
extra_params(includingidandArea) - Encodes to JSON and writes to the sink
- 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_Stateobject 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_cachedprotects 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →