pi-logs
pi-logs
Query pi agent session logs using Nushell's structured data pipeline.
Quick Start
# Load and view recent sessions
ls ~/.pi/agent/sessions/ | get name | first 5
# Query a specific session log
open ~/.pi/agent/sessions/<project-path>/<session-file>.jsonl --raw | lines | each { from json } | first 10
# Find all bash commands run
open-jsonl session.jsonl | where type == "message" | where message.role == "assistant"
Log Structure
Pi agent logs are stored as JSON Lines (.jsonl) files in ~/.pi/agent/sessions/<project-path>/. Each line is a JSON object.
Entry Types
session
Session metadata (first entry in each file).
{
"type": "session",
"version": 3,
"id": "5741eacc-682e-472b-b42a-6d5ebf0105cb",
"timestamp": "2026-04-10T19:12:00.750Z",
"cwd": "/home/knoopx/Projects/knoopx/pi"
}
Fields: version, id, timestamp, cwd
message
User, assistant, or tool messages.
User message:
{
"type": "message",
"id": "7bb32c72",
"parentId": "380e238c",
"timestamp": "2026-04-10T19:12:38.654Z",
"message": {
"role": "user",
"content": [{ "type": "text", "text": "verify skill..." }],
"timestamp": 1775848358650
}
}
Assistant message with tool call:
{
"type": "message",
"id": "cba9a752",
"parentId": "7bb32c72",
"timestamp": "2026-04-10T19:12:43.718Z",
"message": {
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "...",
"thinkingSignature": "reasoning_content"
},
{
"type": "toolCall",
"id": "LkmAH1oV...",
"name": "read",
"arguments": { "path": "..." }
}
],
"api": "openai-completions",
"provider": "local",
"model": "unsloth/Qwen3.5-27B-GGUF",
"usage": {
"input": 12002,
"output": 64,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 12066
},
"stopReason": "toolUse",
"timestamp": 1775848358650,
"responseId": "chatcmpl-..."
}
}
Fields:
- Top-level:
id,parentId,timestamp message.role: "user" or "assistant"message.content[]: Array of content items withtypefield:{ type: "text", text: "..." }{ type: "thinking", thinking: "...", thinkingSignature: "..." }{ type: "toolCall", id: "...", name: "...", arguments: {...} }
message.api: "openai-completions"message.provider: "local", "anthropic", etc.message.model: Model name/IDmessage.usage: Token usage statsmessage.stopReason: "stop", "toolUse", "aborted", etc.
model_change
Model or provider switch.
{
"type": "model_change",
"id": "80e34ca7",
"parentId": null,
"timestamp": "2026-04-10T19:12:00.855Z",
"provider": "local",
"modelId": "unsloth/Qwen3.5-27B-GGUF"
}
Fields: id, parentId, timestamp, provider, modelId
thinking_level_change
Thinking level adjustment (low/medium/high).
{
"type": "thinking_level_change",
"id": "380e238c",
"parentId": "80e34ca7",
"timestamp": "2026-04-10T19:12:00.855Z",
"thinkingLevel": "medium"
}
Fields: id, parentId, timestamp, thinkingLevel
custom_message
Hook output, errors, and VCS notifications.
Hook error:
{
"type": "custom_message",
"customType": "hook-error",
"content": "Hook error:\nESLint: 10.2.0\n...",
"display": true,
"id": "7a51fa2e",
"parentId": "faf0296b",
"timestamp": "2026-04-10T18:39:04.378Z"
}
VCS notification:
{
"type": "custom_message",
"customType": "Squashed change xoqlnppl into change yxqqokrx",
"content": [{ "type": "text", "text": "Working copy (@) now at: ..." }],
"display": true,
"id": "fbdc57fb",
"parentId": "e7ca4d4d",
"timestamp": "2026-04-10T18:47:15.462Z"
}
Fields: customType, content, display, id, parentId, timestamp
customType values:
"hook"- Hook output (eslint, typecheck success)"hook-error"- Hook errors"Squashed change X into change Y"- jj/VCS notifications
compaction
Context compaction summary when session is truncated.
{
"type": "compaction",
"id": "26b7c5ac",
"parentId": "ca53199c",
"timestamp": "2026-04-10T17:09:03.200Z",
"summary": "## Goal\n- ...\n\n## Progress\n...",
"firstKeptEntryId": "5cd3d2eb",
"tokensBefore": 266602,
"details": {
"readFiles": ["..."],
"modifiedFiles": ["..."]
},
"fromHook": false
}
Fields: id, parentId, timestamp, summary, firstKeptEntryId, tokensBefore, details, fromHook
Helper Function
Define this helper once for cleaner queries:
# Add to your env.nu or define inline
def open-jsonl [file: path] {
open $file --raw | lines | each { from json }
}
Core Commands
Loading Session Logs
# Load a single session file
open-jsonl ~/.pi/agent/sessions/<project-path>/<session-file>.jsonl
# Or without helper:
open ~/.pi/agent/sessions/<project-path>/<session-file>.jsonl --raw | lines | each { from json }
# Load all sessions from a project
ls ~/.pi/agent/sessions/<project-path>/*.jsonl
| each { |f| open-jsonl $f.name } | flatten
# Load most recent session
ls ~/.pi/agent/sessions/<project-path>/*.jsonl
| sort-by modified --reverse | first 1 | get name | open-jsonl
Filtering by Type
# Get all messages
open-jsonl session.jsonl | where type == "message"
# Get user messages
open-jsonl session.jsonl | where type == "message" | where message.role == "user"
# Get assistant messages
open-jsonl session.jsonl | where type == "message" | where message.role == "assistant"
# Get tool calls from assistant messages
open-jsonl session.jsonl
| where type == "message"
| where message.role == "assistant"
| where ($in.message.content | any { |c| $c.type == "toolCall" })
# Get hook output (eslint, typecheck, etc.)
open-jsonl session.jsonl | where type == "custom_message"
| select customType content display
# Get hook errors only
open-jsonl session.jsonl | where type == "custom_message"
| where customType == "hook-error"
| select customType content timestamp
# Get VCS notifications (jj squashes, etc.)
open-jsonl session.jsonl | where type == "custom_message"
| where customType | str starts-with "Squashed"
| select customType content timestamp
# Get compaction summaries
open-jsonl session.jsonl | where type == "compaction"
| select timestamp firstKeptEntryId tokensBefore details
Finding Tool Calls
# Extract all tool calls with their names and arguments
open-jsonl session.jsonl
| where type == "message"
| where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall"
| select id name arguments
# Get bash commands specifically
open-jsonl session.jsonl
| where type == "message"
| where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name == "bash"
| get arguments.command
# Get all read operations
open-jsonl session.jsonl
| where type == "message"
| where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name == "read"
| get arguments.path
# Get thinking content
open-jsonl session.jsonl
| where type == "message"
| where message.role == "assistant"
| get message.content
| flatten
| where type == "thinking"
| get thinking
Common Queries
What Commands Were Run?
# All bash commands in a session
open-jsonl session.jsonl
| where type == "message"
| where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name == "bash"
| get arguments.command
# All bash commands with their IDs (for tracking)
open-jsonl session.jsonl
| where type == "message"
| where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name == "bash"
| select id arguments.command
What Hook Errors Occurred?
# All hook errors (eslint, typecheck, etc.)
open-jsonl session.jsonl
| where type == "custom_message" | where customType == "hook-error"
| select timestamp content
# Hook errors with timestamps
open-jsonl session.jsonl
| where type == "custom_message" | where customType == "hook-error"
| select customType timestamp content
Tool Call Analysis
# Count tool calls by type
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall"
| get name | uniq --count
# Get unique tools used
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall"
| get name | uniq
What Files Were Accessed?
# All files read
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name == "read"
| get arguments.path
# All files written (edit/write operations)
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name in ["edit", "write"]
| get arguments.path
# Unique files accessed
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name in ["read", "edit", "write"]
| get arguments.path | uniq
Session Statistics
# Count different entry types
open-jsonl session.jsonl | get type | uniq --count
# Count message roles
open-jsonl session.jsonl
| where type == "message"
| get message.role | uniq --count
# Count tool calls by type
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall"
| get name | uniq --count
# Count custom_message types (hook, hook-error, etc.)
open-jsonl session.jsonl
| where type == "custom_message"
| get customType | uniq --count
# Token usage summary
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.usage
| flatten
| math sum
Find Specific Patterns
# Find sessions where a specific command was run
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name == "bash"
| where ($in.arguments.command | str contains "jj diff")
# Find all grep operations
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name == "bash"
| where ($in.arguments.command | str contains "grep")
| get arguments.command
# Find file modifications
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name in ["edit", "write"]
| select id name arguments
User Queries Analysis
# Get all user messages
open-jsonl session.jsonl
| where type == "message" | where message.role == "user"
| get message.content
| flatten
| where type == "text"
| get text
# Count user messages
open-jsonl session.jsonl
| where type == "message" | where message.role == "user"
| length
# Get user messages with timestamps
open-jsonl session.jsonl
| where type == "message" | where message.role == "user"
| select timestamp message
Model and Provider Info
# Get model changes
open-jsonl session.jsonl | where type == "model_change"
| select provider modelId timestamp
# Get thinking level changes
open-jsonl session.jsonl | where type == "thinking_level_change"
| select thinkingLevel timestamp
# Get token usage from assistant messages
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| select message.usage message.model
Custom Commands
# Quick session stats
def "session-stats" [file: path] {
let data = (open-jsonl $file)
{
total_events: ($data | length)
messages: ($data | where type == "message" | length)
user_messages: ($data | where type == "message" | where message.role == "user" | length)
assistant_messages: ($data | where type == "message" | where message.role == "assistant" | length)
tool_calls: ($data | where type == "message" | where message.role == "assistant" | get message.content | flatten | where type == "toolCall" | length)
custom_messages: ($data | where type == "custom_message" | length)
hook_errors: ($data | where type == "custom_message" | where customType == "hook-error" | length)
compactions: ($data | where type == "compaction" | length)
session_id: ($data | where type == "session" | get id | first)
}
}
# Get model info
def "model-stats" [file: path] {
open-jsonl $file
| where type == "message" | where message.role == "assistant"
| select message.model message.provider message.usage
| first
}
# Get hook errors
def "hook-errors" [file: path] {
open-jsonl $file
| where type == "custom_message" | where customType == "hook-error"
| select timestamp content
}
# Get bash commands
def "bash-history" [file: path] {
open-jsonl $file
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name == "bash"
| get arguments.command | uniq
}
# Get all custom messages by type
def "custom-messages" [file: path] {
open-jsonl $file
| where type == "custom_message"
| get customType | uniq --count
}
# Find files modified
def "files-modified" [file: path] {
open-jsonl $file
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name in ["edit", "write"]
| get arguments.path | uniq
}
# Get compaction summaries
def "compactions" [file: path] {
open-jsonl $file
| where type == "compaction"
| select timestamp firstKeptEntryId tokensBefore
| each { |c|
{
timestamp: $c.timestamp
tokensBefore: $c.tokensBefore
readFiles: ($c.details.readFiles | length)
modifiedFiles: ($c.details.modifiedFiles | length)
}
}
}
Multi-Session Queries
# Compare tool usage across sessions
ls ~/.pi/agent/sessions/<project-path>/*.jsonl | each { |f|
let data = (open-jsonl $f.name)
let tool_calls = ($data | where type == "message" | where message.role == "assistant" | get message.content | flatten | where type == "toolCall" | get name | uniq --count)
{
file: $f.name
tools: $tool_calls
}
}
# Aggregate statistics across all sessions
ls ~/.pi/agent/sessions/<project-path>/*.jsonl | each { |f|
open-jsonl $f.name
} | flatten | where type == "message" | where message.role == "assistant" | get message.content | flatten | where type == "toolCall" | get name | uniq --count
Practical Examples
Debug a Failed Session
# Load session and find what went wrong
let session = (open-jsonl "failed_session.jsonl")
# Check for aborted messages
$session | where type == "message" | where message.role == "assistant" | where message.stopReason == "aborted"
# Check for error messages
$session | where type == "message" | where message.role == "assistant" | where message.errorMessage != null
# Check for hook errors (eslint, typecheck failures)
$session | where type == "custom_message" | where customType == "hook-error"
| select timestamp content
Find How to Run a Command
# Search for a specific command pattern
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name == "bash"
| where ($in.arguments.command | str contains "bun run")
| get arguments.command
# Find all build commands
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name == "bash"
| where ($in.arguments.command | str contains --ignore-case "build" or $in.arguments.command | str contains "bun run")
| get arguments.command | uniq
Track File Changes Over Time
# Get all file operations in order
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name in ["read", "edit", "write"]
| select timestamp name arguments.path
# Group by file
open-jsonl session.jsonl
| where type == "message" | where message.role == "assistant"
| get message.content
| flatten
| where type == "toolCall" | where name in ["read", "edit", "write"]
| select timestamp name path: arguments.path
| group-by path
Constraints
- Session paths contain encoded directory paths with
--prefix/suffix - JSON Lines format: each line is a separate JSON object
- Tool calls are embedded in assistant messages under
message.content[]withtype == "toolCall" - Timestamps are ISO 8601 format (string) or Unix milliseconds (number)
- Use
flattento extract nested arrays frommessage.content - Some fields may be
null- filter withwhere $it != null custom_message.customType: "hook" for output, "hook-error" for errors, "Squashed change..." for VCScompactionentries contain summarized context when sessions are truncated- Entry IDs are unique within a session;
parentIdlinks related entries
Tips
- Use
jqfor complex JSON transformations if nushell struggles - Save filtered results:
open session.jsonl | where ... | save filtered.jsonl - Load multiple sessions:
ls *.jsonl | each { |f| open-jsonl $f.name } | flatten - Use
str contains --ignore-casefor case-insensitive command searches - Pipe to
tableorto mdfor better formatting in reports - Tool calls don't have separate result entries; results are shown in subsequent assistant messages
- Use
message.usagefields to track token consumption per assistant response
More from knoopx/pi
podman
Manages containers, builds images, configures pods and networks with Podman. Use when running containers, creating Containerfiles, grouping services in pods, or managing container resources.
122jujutsu
Manages version control with Jujutsu (jj), including rebasing, conflict resolution, and Git interop. Use when tracking changes, navigating history, squashing/splitting commits, or pushing to Git remotes.
117nix-flakes
Creates reproducible builds, manages flake inputs, defines devShells, and builds packages with flake.nix. Use when initializing Nix projects, locking dependencies, or running nix build/develop commands.
54scraping
Fetches web pages, parses HTML with CSS selectors, calls REST APIs, and scrapes dynamic content. Use when extracting data from websites, querying JSON APIs, or automating browser interactions.
48jscpd
Finds duplicate code blocks and analyzes duplication metrics across files. Use when identifying copy-pasted code, measuring technical debt, or preparing for refactoring.
45yt-dlp
Downloads videos from YouTube and other sites using yt-dlp. Use when downloading videos, extracting metadata, or batch downloading multiple files.
42