Event journal

Every Hankweave run leaves behind a durable, line-by-line record of what happened: which codons started and finished, what the agent did, what files changed, what it cost, and what went wrong. That record is the event journal, a JSONL file inside the execution directory. Because it is plain JSON Lines, we can inspect it with jq during a live run, load it into a script afterward, or parse it with the schemas the published package exports.

This page walks through the journal in the order we would use it: what it captures and what it deliberately leaves out, where to find it on disk, how the storage backend behaves, how to read and filter it, and the pitfalls that matter when we turn raw events into summaries. By the end we will be able to answer questions like "why did this codon fail?" and "what did this run cost?" from the journal alone.

What the journal captures (and what it doesn't)#

The journal is the append-only JSONL file .hankweave/events/events.jsonl inside an execution directory. Hankweave constructs it with file-backed storage after setting up the .hankweave directory, and it initializes the journal (restoring a dieted journal first, if the execution has one) before the server starts listening, so the file is ready before the run begins emitting server events.

Not every event the runtime produces ends up in the file. The journal accepts three event categories:

  • Server-state events describe execution state and lifecycle, including start and completion events, transitions, errors, budgets, checkpoints (saved execution points), and rollbacks.
  • Agentic-backbone events record agent actions, tool results, file changes, setup, and command output.
  • Sentinel events record observer lifecycle, triggers, output, and errors. Sentinel configuration and trigger behavior belong to sentinel configuration; the parallel-observer model is explained in sentinels.

Connection-state events are the boundary: they go to specific WebSocket clients, are not part of the permanent execution history, and are never journaled. Of the four declared connection-state types, the server actually sends server.ready, pong, and history.batch; incomplete.codon is declared in the schemas but no runtime code emits it, so a client never receives one. If a connection-state event is passed to EventJournal.append(), the method throws instead of journaling it. The catalog has 36 event types, 32 of them journaled: 20 server-state, 7 agentic-backbone, and 5 sentinel types. The four connection-state types are the remaining four.

FIG. 1 Only three event categories enter the journal. Connection-state events go to WebSocket clients instead.
Read the diagram as text
Output
runtime event category                  destination

server-state (20) ------+
agentic-backbone (7) ---+-- isJournaledEvent --> .hankweave/events/events.jsonl
sentinel (5) -----------+

connection-state (4) ------ direct transport --> WebSocket clients
connection-state (4) ---x journal rejects it (append throws)

The category is inferred from the event type, not stored as a category field. The runtime checks membership in four compile-time-verified type sets; isJournaledEvent(event) is the combined predicate for the three persisted categories. A journal therefore tells you what the runtime recorded, but not every message exchanged with a client.

One qualification to the figure above: targeted events, such as a command-refusal error sent to a single client, go only to their target socket and are neither journaled nor broadcast. isJournaledEvent classifies the event type, while the emit path chooses the destination.

The file is cumulative for the life of its execution directory. Restarting or resuming with the same execution directory appends to the existing file rather than replacing it. The runtime uses the same file-backed journal on each run; its append queue preserves emission order, so do not use timestamps as a promise of file order.

Finding the journal on disk#

Resolve the path relative to the execution directory, not relative to the project or configuration that supplied it. The {timestamp}-{random}-{hash} text below is a placeholder, not a literal directory name: replace it with the path printed when Hankweave creates the execution (or inspect ls -t "$HOME/.hankweave-executions"), and run commands using .hankweave/... from inside that execution directory. These commands locate the journal and confirm it exists and is non-empty:

⌁ Terminal
EXECUTION="$HOME/.hankweave-executions/{timestamp}-{random}-{hash}"
EVENTS="$EXECUTION/.hankweave/events/events.jsonl"
ls -l "$EVENTS"
test -s "$EVENTS"

The events/ directory is created lazily by file storage. Initialization creates its parent directories and an empty events.jsonl when the file is absent. Hankweave ships a .gitignore only with the --init scaffold (which ignores .hankweave/, *.log, and node_modules/ for a project created that way); the runtime writes no ignore rule for the journal itself, so whether it is committed depends on your own repository's ignore rules. For the rest of the execution-directory tree, see the execution directory reference.

Once we have the file, its structure is simple: each journal line is one compact JSON object, and every journaled event has the same four top-level fields:

Scroll to explore the table →
FieldMeaning
idA runtime-generated string identifier; it has no semantic prefix.
typeA dot-separated event type literal, such as state.transition or codon.completed.
timestampAn ISO 8601 timestamp for the event.
dataThe payload specific to type.

The writer uses JSON.stringify, so each event occupies one dense line, with no wrapper array, trailing comma, or pretty-printing. That shape means standard JSONL tools work directly on the file. This command prints the top-level keys of the first record (on a live journal, or after restoring a finished one as described under When the journal grows too large):

⌁ Terminal
EVENTS="$HOME/.hankweave-executions/{timestamp}-{random}-{hash}/.hankweave/events/events.jsonl"
head -1 "$EVENTS" | jq 'keys'

The jq check reports the alphabetized keys data, id, timestamp, and type. During a run, we can watch newly appended records as they arrive:

⌁ Terminal
tail -f .hankweave/events/events.jsonl | jq '.type'

Two things are worth knowing before reading a journal top to bottom. The first event in the minimal single-provider capture is a state.transition with data.transitionType equal to RunStarted; do not assume that codon.started is the first line. And the file's storage order is emission order, which can differ from timestamp order: the sample below places a token.usage record with an earlier timestamp after file.updated.

Every file.updated event carries exactly six fields, path, filename, action, sha256, bytes, and source, with no inline content body. source is { kind: "tool_use", toolUseId } for a file the agent wrote, or { kind: "codon-start" } for one present when the codon started. (A sentinel condition may still name a virtual content field, which the sentinel's view resolves at trigger time; that is a separate contract from the wire payload.) Here is a real line from the execution-directory capture:

JSONL
{"id":"<id>","timestamp":"<ts>","type":"file.updated","data":{"path":"out.txt","filename":"out.txt","action":"modified","sha256":"f55c80f6dcc76d5a821fe0a177a72e7664eeb0525e7356b7f47157246fc9a5d6","bytes":20,"source":{"kind":"tool_use","toolUseId":"toolu_01P8J9uPJjEpXzBZu5gFTCXj"}}}

The excerpt below is a real nine-record capture used by the published-package schema consumer, recorded on 2026-09-02 against hankweave@0.10.0 and carried unchanged into this edition; its file.updated line still carries the old inline content body, so read it as an earlier-release recording and apply the six-field shape above when filtering current journals. The run's single codon summarized a meeting-notes file: it opens with a state.transition, the codon's work appears as codon.started, assistant.action, tool.result, and file.updated records, and the run closes with cost and completion records. It is an excerpt, not a complete journal or a per-run volume measurement.

JSONL
{"id":"1788337916074-d359hfspa","timestamp":"2026-09-02T08:31:56.074Z","type":"state.transition","data":{"transitionType":"RunStarted","runId":"1788337916070-aqhfn","transition":{"type":"RunStarted","data":{"runId":"1788337916070-aqhfn","runFolder":"~/.hankweave-executions/1788337915373-x194-4a876b/.hankweave/runs/1788337916070-aqhfn","gitBranch":"run-1788337916070-aqhfn","startingConditions":{"type":"fresh"},"serverPid":52624}},"resultingState":{"currentRunId":"1788337916070-aqhfn","runCount":1,"totalCost":0,"currentRunCost":0}}}
{"id":"1788337919229-5vx1pqbn5","timestamp":"2026-09-02T08:31:59.229Z","type":"codon.started","data":{"codonId":"summarize-notes","codonName":"Summarize the notes","sessionId":"83b52eaa-5147-4130-aef4-818c9c663c77","startTime":"2026-09-02T08:31:56.104Z"}}
{"id":"1788337922234-f4o24pmlt","timestamp":"2026-09-02T08:32:02.234Z","type":"assistant.action","data":{"codonId":"summarize-notes","action":"thinking","content":"The user wants me to:\n1. Read `notes.txt` from `read_only_data_source` directory in the current working directory\n2. Write `summary.md` in the current working directory with:\n   - A one-sentence summary of what the meeting decided\n   - Exactly three bullet points: one decision, one owner, one deadline\n   - Nothing else, under 15 lines\n\nLet me start by reading the notes file. I should use the shell to do this, since the user mentions I can use shell commands like `cat`."}}
{"id":"1788337923235-bgt2n27vh","timestamp":"2026-09-02T08:32:03.235Z","type":"tool.result","data":{"codonId":"summarize-notes","toolUseId":"toolu_01J8vWtreamPHhvdybo9n2FG","toolName":"Bash","result":"FICTIONAL TRAINING FIXTURE — invented content for documentation testing.\n\nMeeting notes, Tuesday.\nAttendees: Mara, Devon, Sam.\nWe compared the two vendor quotes for the packaging line refit.\nDecision: go with the Calloway quote (lower total, includes installation).\nDevon owns the purchase order and vendor follow-up.\nDeadline: PO issued by Friday the 12th.\nSam raised spare-parts stocking; parked for next week.","truncated":false,"originalLength":412,"executionTimeMs":1001,"isError":false}}
{"id":"1788337925241-w3x9qrqmz","timestamp":"2026-09-02T08:32:05.241Z","type":"file.updated","data":{"path":"summary.md","filename":"summary.md","content":"# Meeting Summary\n\nThe team selected the Calloway vendor quote for the packaging line refit based on lower total cost and included installation services.\n\n- **Decision:** Go with Calloway quote for packaging line refit (lower cost, includes installation)\n- **Owner:** Devon (purchase order and vendor follow-up)\n- **Deadline:** PO issued by Friday the 12th\n","action":"modified"}}
{"id":"1788337922233-lymp156f6","timestamp":"2026-09-02T08:32:02.233Z","type":"token.usage","data":{"codonId":"summarize-notes","inputTokens":9,"outputTokens":1,"cacheCreationTokens":0,"cacheReadTokens":18715,"totalCost":0.0018854999999999998,"modelId":"claude-haiku-4-5"}}
{"id":"1788337928090-rxcskk0dz","timestamp":"2026-09-02T08:32:08.090Z","type":"codon.completed","data":{"codonId":"summarize-notes","success":true,"cost":0.01050655,"duration":11986,"exitStatus":{"type":"success"}}}
{"id":"1788337930187-b3abvvs5h","timestamp":"2026-09-02T08:32:10.187Z","type":"budget.summary","data":{"ceiling":{},"allocation":"shared","rows":[{"codonId":"summarize-notes","status":"completed","budget":{},"actual":{"dollars":0.01050655,"timeSeconds":11.975,"outputTokens":654}}],"totals":{"actualDollars":0.01050655,"actualTimeSeconds":11.975}}}
{"id":"1788337930188-jrvuw0nlz","timestamp":"2026-09-02T08:32:10.188Z","type":"state.transition","data":{"transitionType":"RunCompleted","runId":"1788337916070-aqhfn","transition":{"type":"RunCompleted","data":{"runId":"1788337916070-aqhfn"}},"resultingState":{"currentRunId":null,"runCount":1,"totalCost":0.01050655,"currentRunCost":0}}}

How events are stored: the two backends#

The runtime does not require you to choose a backend: it always constructs FileEventStorage for the execution journal. That backend writes .hankweave/events/events.jsonl, counts existing non-empty lines when initialized, increments its in-memory count as events are appended, and persists across server restarts. While the journal is live it also keeps an advisory sidecar, events.meta.json, next to the journal, recording schemaVersion, eventSchemaVersion, totalEvents, byteLength, and lastEventId; the runtime writes it after a startup rescan, every 1,000 appended events, and when the journal closes. The journal file is the only source of truth: a missing or stale sidecar costs nothing but a rescan.

The writer holds events.jsonl open for the storage's lifetime (the descriptor is opened once at initialization and reused by every append), and concurrent appends are serialized through an internal write chain, so lines cannot interleave. Crash safety comes from the append-only format plus a tolerant reader, not from fsync.

append(event) delegates to appendMany([event]); bulk appends flush in chunks of 10,000 lines, which affects only callers that submit bulk iterables.

MemoryEventStorage is the alternative implementation used when EventJournal is constructed without a storage argument. It keeps at most 100 events in memory. When it exceeds that limit, it drops at least max(10, excess) entries, so older events can disappear. It has no durable file and loses all events on restart; treat it as a test/development transport rather than the runtime's persistent journal.

An implementation can be substituted through the IEventStorage contract. It must provide initialize(), append(), appendMany(), getRecentEvents(), getEventsAfter(), getTotalEvents(), createReadStream(), and close(). The interface is the extension point; the shipped runtime's persistence behavior comes from FileEventStorage.

Reading events from the journal#

For external consumers, read the JSONL file directly; EventJournal is an internal server class, not the public client API. Its query helpers are still worth knowing, because they define the views the server itself offers and the behavior any reader should expect:

  • getMostRecentEvents(limit) returns up to limit events newest first, together with totalEvents, hasMore, and a corruptLines count of unparseable lines skipped while reading. A handshake requests this view using an internal handshakeHistoryLimit default of 50; that value is not a hankweave.json key, an environment variable, or a CLI flag, so a client that needs more history reads with history.sync.
  • getAllEvents() is an async generator that walks the journal in cursor pages and yields one event at a time, tolerant of corrupt lines. Use this for a large journal instead of loading the whole file into memory.
  • getEventsAfter(cursor, limit) is the storage-level paging query underneath it: a page of events, an opaque nextCursor, hasMore, and a corruptLines count.
  • getTotalEvents() returns the backend's in-memory count. File storage seeds it from the sidecar (or a rescan when the sidecar is absent or stale) and increments it on append.
  • streamAllEvents() exposes the raw readable stream, which can be piped to another process, gzip, or an HTTP response; for a dieted directory it streams the logical journal transparently.

A history.sync WebSocket command uses getAllEvents() and sends the full journal in history.batch responses. Those response messages are connection-state traffic and are not added back to the journal. The runtime also subscribes to emitted events for CLI telemetry; that subscription is separate from journal storage, and its sends are awaited with a 2-second timeout per step, so they can delay startup or an early-exit command slightly but never throw.

For a small file, the same JSONL shape can be loaded into an array. For a large file, page through it instead of loading everything:

TYPESCRIPT
const events = fs
  .readFileSync('.hankweave/events/events.jsonl', 'utf8')
  .trim()
  .split('\n')
  .map((line) => JSON.parse(line));

While a run is live, the journal is append-only: records are not modified, deleted, or reordered after they are written. A malformed line no longer fails a read: the storage skips unparseable lines and reports them in corruptLines, and initialization heals a torn final line so new appends start on a fresh line. (Finalization later compresses the closed file without changing the logical journal; see When the journal grows too large.)

Filtering events for your task#

Start with the event type, then inspect the type-specific data payload. Most events carry data.codonId, which lets us isolate one codon's activity while iterating the journal. The table below is the full catalog: which types exist, which category each belongs to, whether it is journaled and sentinel-routed, and the payload fields to filter on. The event reference covers the same catalog with its schemas.

Scroll to explore the table →
idcategoryjournaledsentinelRoutedpayloadFieldsreceipts
archive.completedserver-statetruetruecodonId, archivedPathsschemas/event-schemas.ts:780, schemas/event-schemas.ts:1091, schemas/event-schemas.ts:1371, hankweave-runtime.ts:5784
archive.partialserver-statetruetruecodonId, archivedPaths, failedPathsschemas/event-schemas.ts:785, schemas/event-schemas.ts:1092, schemas/event-schemas.ts:1372, hankweave-runtime.ts:5769
assistant.actionagentic-backbonetruetruecodonId, action, content, toolName?, toolInput?, toolUseId?schemas/event-schemas.ts:684, schemas/event-schemas.ts:1100, schemas/event-schemas.ts:1351, hankweave-runtime.ts:3210
budget.summaryserver-statetruetrueceiling, allocation, rows, totalsschemas/event-schemas.ts:836, schemas/event-schemas.ts:1093, schemas/event-schemas.ts:1381, hankweave-runtime.ts:7086
checkpoint.listserver-statetruetruerunId, checkpoints, currentBranchschemas/event-schemas.ts:749, schemas/event-schemas.ts:1082, schemas/event-schemas.ts:1364, hankweave-runtime.ts:4612
codon.completedserver-statetruetruecodonId, success, cost, duration, exitStatus, failureReason?, failureIgnored?, budgetExceeded?schemas/event-schemas.ts:672, schemas/event-schemas.ts:1075, schemas/event-schemas.ts:1349, hankweave-runtime.ts:3792
codon.extendedserver-statetruetruecodonId, codonName, extensionNumber, exhaustWithPrompt, cumulativeTokens, cumulativeCostschemas/event-schemas.ts:677, schemas/event-schemas.ts:1076, schemas/event-schemas.ts:1350, hankweave-runtime.ts:3378
codon.startedserver-statetruetruecodonId, codonName, codonDescription?, sessionId, previousSessionId?, startTime, promptMetadata?schemas/event-schemas.ts:667, schemas/event-schemas.ts:1074, schemas/event-schemas.ts:1348, hankweave-runtime.ts:385
errorserver-statetruetruemessage, codon?, fatal, severity?, context?, code?schemas/event-schemas.ts:729, schemas/event-schemas.ts:1081, schemas/event-schemas.ts:1360, hankweave-runtime.ts:1214
file.updatedagentic-backbonetruetruepath, filename, action, sha256, bytes, sourceschemas/event-schemas.ts:699, schemas/event-schemas.ts:1102, schemas/event-schemas.ts:1354, hankweave-runtime.ts:3027
filetree.updatedagentic-backbonetruetruetreeschemas/event-schemas.ts:704, schemas/event-schemas.ts:1103, schemas/event-schemas.ts:1355, hankweave-runtime.ts:3039
history.batchconnection-statefalsefalseevents, hasMoreschemas/event-schemas.ts:800, schemas/event-schemas.ts:1126, schemas/event-schemas.ts:1374, hankweave-runtime.ts:1563
incomplete.codonconnection-statefalsefalsecodonId, codonName, messageschemas/event-schemas.ts:734, schemas/event-schemas.ts:1127, schemas/event-schemas.ts:1361
infoserver-statetruetruemessageschemas/event-schemas.ts:739, schemas/event-schemas.ts:1080, schemas/event-schemas.ts:1362, hankweave-runtime.ts:2102
loop.iteration.completedserver-statetruetrueloopId, iteration, durationMs, costUsd, tokensUsed, isFinal, terminationReason?schemas/event-schemas.ts:724, schemas/event-schemas.ts:1090, schemas/event-schemas.ts:1359, hankweave-runtime.ts:6415
pongconnection-statefalsefalsemessage, timestamp, clientId?schemas/event-schemas.ts:795, schemas/event-schemas.ts:1125, schemas/event-schemas.ts:1373, hankweave-runtime.ts:1466
rig.outputagentic-backbonetruetruecodonId, stream, line, commandIndexschemas/event-schemas.ts:719, schemas/event-schemas.ts:1106, schemas/event-schemas.ts:1358, hankweave-runtime.ts:6489
rig.setup.completedagentic-backbonetruetruecodonId, rigType, commandCount, durationMs, createdCheckpointschemas/event-schemas.ts:709, schemas/event-schemas.ts:1104, schemas/event-schemas.ts:1356, hankweave-runtime.ts:2604
rig.setup.failedagentic-backbonetruetruecodonId, failureType, exitCode?, commandIndex?, ignoredschemas/event-schemas.ts:714, schemas/event-schemas.ts:1105, schemas/event-schemas.ts:1357, hankweave-runtime.ts:2213
rollback.archiveRestoreserver-statetruetruecodonId, restoredPaths, failedPaths?, statusschemas/event-schemas.ts:790, schemas/event-schemas.ts:1088, schemas/event-schemas.ts:1370, hankweave-runtime.ts:5925
rollback.codonCheckpointserver-statetruetruecodonId, codonName, checkpoint, checkpointType, messageschemas/event-schemas.ts:759, schemas/event-schemas.ts:1085, schemas/event-schemas.ts:1366, hankweave-runtime.ts:5048
rollback.completedserver-statetruetruefromRun, toRun, checkpoint, codonId, codonName, checkpointType, autoRestartschemas/event-schemas.ts:774, schemas/event-schemas.ts:1086, schemas/event-schemas.ts:1369, hankweave-runtime.ts:5075
rollback.progressserver-statetruetruecurrentStep, totalSteps, messageschemas/event-schemas.ts:769, schemas/event-schemas.ts:1084, schemas/event-schemas.ts:1368, hankweave-runtime.ts:5469
rollback.rigCleanupserver-statetruetruecodonId, codonName, directories, status, successfulCleanups?, failedCleanups?, error?schemas/event-schemas.ts:764, schemas/event-schemas.ts:1087, schemas/event-schemas.ts:1367, hankweave-runtime.ts:5998
rollback.startedserver-statetruetruefromRun, fromCodon, toCodon, toCheckpoint, checkpointType, codonsToProcessschemas/event-schemas.ts:754, schemas/event-schemas.ts:1083, schemas/event-schemas.ts:1365, hankweave-runtime.ts:5010
sentinel.errorsentineltruefalsesentinelId, codonId, errorType, message, retriable, consecutiveFailureCountschemas/event-schemas.ts:821, schemas/event-schemas.ts:1115, schemas/event-schemas.ts:1378, sentinels/sentinel.ts:576
sentinel.loadedsentineltruefalsesentinelId, codonId, model, triggerType, executionStrategy, conversational, source, sourcePath?schemas/event-schemas.ts:811, schemas/event-schemas.ts:1113, schemas/event-schemas.ts:1376, hankweave-runtime.ts:6201
sentinel.outputsentineltruefalsesentinelId, codonId, triggerNumber, outputType, content, cost, tokens, eventCountschemas/event-schemas.ts:826, schemas/event-schemas.ts:1116, schemas/event-schemas.ts:1379, sentinels/sentinel.ts:538
sentinel.triggeredsentineltruefalsesentinelId, codonId, triggerNumber, strategy, eventCount, queueSizeschemas/event-schemas.ts:831, schemas/event-schemas.ts:1117, schemas/event-schemas.ts:1380, sentinels/sentinel.ts:392
sentinel.unloadedsentineltruefalsesentinelId, codonId, reason, errorType?, finalCost, llmCallCountschemas/event-schemas.ts:816, schemas/event-schemas.ts:1114, schemas/event-schemas.ts:1377, sentinels/sentinel-manager.ts:908
server.idleserver-statetruetruereason, messageschemas/event-schemas.ts:744, schemas/event-schemas.ts:1078, schemas/event-schemas.ts:1363, hankweave-runtime.ts:1180
server.readyconnection-statefalsefalseserverVersion, executionPath, agentRootPath, dataPath, port, proxyPort?, outputDirectory?schemas/event-schemas.ts:657, schemas/event-schemas.ts:1124, schemas/event-schemas.ts:1346, hankweave-runtime.ts:1155
state.snapshotserver-statetruetruecurrentCodon?, completedCodons, fileTree, totalCost, totalTime, recentFileAccess?, isRollingBackschemas/event-schemas.ts:662, schemas/event-schemas.ts:1077, schemas/event-schemas.ts:1347, hankweave-runtime.ts:1676
state.transitionserver-statetruetruetransitionType, runId?, codonId?, transition, resultingStateschemas/event-schemas.ts:805, schemas/event-schemas.ts:1089, schemas/event-schemas.ts:1375, hankweave-runtime.ts:463
token.usageserver-statetruetruecodonId, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, totalCost, modelId?, modelUsage?schemas/event-schemas.ts:689, schemas/event-schemas.ts:1079, schemas/event-schemas.ts:1352, hankweave-runtime.ts:3075
tool.resultagentic-backbonetruetruecodonId, toolUseId, toolName, result, truncated, originalLength, executionTimeMs, isErrorschemas/event-schemas.ts:694, schemas/event-schemas.ts:1101, schemas/event-schemas.ts:1353, hankweave-runtime.ts:3338

In this catalog, toolName? and toolUseId? on assistant.action are optional only for thinking and message actions; a tool_use action must carry both, and the runtime's wire schema rejects an event that omits either. toolUseId is the join key to tool.result.toolUseId and file.updated.source.toolUseId.

For category-aware TypeScript, import the public classifiers from hankweave/schemas:

TYPESCRIPT
  isAgenticBackboneEvent,
  isConnectionStateEvent,
  isSentinelEvent,
  // Event category classifiers
  isServerStateEvent,

The four guards are isServerStateEvent, isAgenticBackboneEvent, isSentinelEvent, and isConnectionStateEvent. The first three identify records suitable for the journal; the fourth identifies client-specific records that are not persisted.

Useful filters include:

  • Failed codons: select event.type === "codon.completed" && !event.data.success; the typed event.data.failureReason includes its failure type and retriable flag. See errors and exit codes for the failure taxonomy.
  • Errors: select event.type === "error". Its data.fatal field is required; data.severity, when present, is one of fatal, codon, operation, or warning.
  • Time windows: compare the ISO 8601 timestamp lexicographically. For example, this streams JSONL records into an array before filtering (on a live journal, or after restoring a finished one):
⌁ Terminal
jq -s '[.[] | select(.timestamp > "2026-09-01T12:00:00Z")]' .hankweave/events/events.jsonl

To build a progress view, we can treat codon.started as running and codon.completed as success or failure while recording its cost; an error whose data.codon identifies the codon supplies the failure note. A recent-event view of 100 records is enough for that live display, but it is bounded and can miss older activity.

Treat cost records carefully. Each codon.completed event carries that attempt's own cost plus the immediately preceding failed attempt's cost, and nothing earlier; the retry accumulator is cleared at each emit. Keeping only the last completion per (runId, codonId) is therefore exact for a codon with at most one retry and under-counts after two or more (the default maxAttempts: 3 allows four attempts); summing every completion double-counts. Completion records carry codonId but not runId, so group them under runs using the state.transition records, whose payloads carry runId (see the catalog above). Do not add token.usage.totalCost on top: token.usage mixes incremental per-message deltas with a final total, and modelUsage can be absent even on that final record.

One more scope to know: budget.summary is emitted only inside a graceful shutdown of a run with a budget configured, before that shutdown's RunCompleted/RunFailed. A run failed by a codon or output-copy failure records RunFailed first and ends with no budget.summary, so run-end totals cannot be read from that event in general. Budget mechanics are owned by budgets and the hankweave.json reference.

Event journal vs. WebSocket log: which one you want#

Use the event journal for post-run analysis of what the runtime recorded. It contains server-state, agentic-backbone, and sentinel events, but not client commands or connection-state events, and it is the record of server traffic.

The runtime configuration declares a WebSocket-log path with the default .hankweave/logs/websocket.log, and the schema describes an intended JSONL entry shape (loggedAt, direction, message, optional metadata.size). No runtime code path writes that file, however: the log writer is reachable only through a deprecated wrapper that has no callers, and nothing reads the configured path. Treat the path and its entry shape as declared-but-unwritten configuration, not as a record a run leaves. For wire-level questions, read the event journal.

When the journal grows too large#

While a run is live the journal grows monotonically: the runtime never rotates, archives, or truncates it. The available captures do not support a general "events per codon" or bytes-per-event estimate, so measure a complete journal from your own workload if you need capacity planning.

At shutdown, after the journal closes, finalization can diet the closed journal. The diet runs when dietOnFinalize is on (the default), the runtime can still prove it owns runtime.lock, and the final run ended completed or failed. Otherwise server.log records Journal diet skipped: this runtime cannot prove it still owns runtime.lock or Journal diet skipped: run did not reach a terminal state, and the original events.jsonl stays in place. A diet moves inline bodies of at least 4,096 bytes into events/cas/<first two hex>/<sha256>.zst (only those, so a run with no large body has no cas/), compresses the pointer journal whole to events.jsonl.zst, and writes journal.meta.json (which carries originalSha256, originalBytes, totalEvents, segments, dietedAt, and the eventSchemaVersion carried over from the sidecar). Before removing events.jsonl and events.meta.json, the diet proves from disk that the rewritten pointer journal and CAS bodies rebuild the original byte for byte.

A finished run therefore leaves either the dieted form (events.jsonl.zst, journal.meta.json, and cas/ when a body was extracted) or, when the diet was off, skipped, or failed, the plain events.jsonl with its events.meta.json sidecar. To read a dieted journal offline, restore it first:

⌁ Terminal
bunx hankweave@0.11.0 --restore-journal <exec>

That rebuilds events.jsonl in place, verifies it against originalSha256, and leaves events.jsonl.zst and journal.meta.json beside it (replace <exec> with your execution directory). Resuming or rerunning the execution restores the journal automatically before appending, and --diet-journal <exec> diets offline under the same lock rules. The two flags are documented in the CLI reference.

If a diet was skipped or you run with dietOnFinalize off, the journal stays in its plain form, and compressing it between runs remains possible:

⌁ Terminal
EVENTS="$HOME/.hankweave-executions/{timestamp}-{random}-{hash}/.hankweave/events/events.jsonl"
gzip -c "$EVENTS" > "$EVENTS.gz"
mv "$EVENTS" "$EVENTS.previous"

Do this only between runs, and prefer --diet-journal, which keeps the byte-identical restore guarantee. During a run, the runtime serializes appends through eventJournalAppendQueue, preserving emission order under concurrent bursts. On clean shutdown it awaits that queue before returning, so the queued file writes have drained before shutdown completes.

After a run that reached a terminal state with the diet applied, ls .hankweave/events/ shows events.jsonl.zst and journal.meta.json (no events.jsonl); with the diet skipped it shows events.jsonl instead. The execution-directory capture records the diet in its server log (Event journal dieted: 15370 → 3104 bytes (0 bodies in CAS, restore with --restore-journal)), and its tree listing shows the post-restore state with events.jsonl beside the diet pair.

Reading from disk with real import paths#

The published library surfaces for event parsing are under hankweave/schemas and hankweave/types; the @hankweave/types import path shown in older documentation does not exist, and the package exports only hankweave, hankweave/schemas, and hankweave/types. Parse each JSONL line with the public union schema:

TYPESCRIPT
import { serverEventSchema } from "hankweave/schemas";

const event = serverEventSchema.parse(JSON.parse(line));

For large journals, pair fs.createReadStream with readline and parse line by line rather than loading the whole file:

TYPESCRIPT
const rl = readline.createInterface({
  input: fs.createReadStream('.hankweave/events/events.jsonl'),
  crlfDelay: Infinity,
});

for await (const line of rl) {
  const event = serverEventSchema.parse(JSON.parse(line));
  // handle event
}

The same hankweave/schemas export provides per-event schemas such as codonStartedEventSchema, codonCompletedEventSchema, and toolResultEventSchema for targeted parsing. The separate hankweave/types export contains state and branded-ID types for consumers that also read state.json; see client and exported types.

Watch out: duplicate completion events#

A retrying or resumed codon emits codon.completed once for each terminal attempt. The same codonId can therefore appear in multiple completion records. Loop repetitions are different: their runtime codon IDs can be distinct, so they add completion records without being deduplicated by the retry key; a captured three-codon loop run (2026-09-03, hankweave@0.10.0) produced seven completion events for its three codons.

When summarizing spend or status, keep run boundaries from state.transition and retain the last completion for each (runId, codonId). Remember the limit from Filtering events for your task: that recipe is exact for at most one retry and under-counts after two or more. Do not sum every completion for a retried codon, and do not add token.usage totals to the completion cost. A codon.extended event is separate: exhaustWithPrompt emits one for each extension and includes its extension number, cumulative tokens, and cumulative cost.

To check a journal for repeated completions, use -s so jq reads the JSONL stream as one array (on a live journal, or after bunx hankweave@0.11.0 --restore-journal <exec> for a finished one):

⌁ Terminal
jq -s '[.[] | select(.type == "codon.completed")] | length' .hankweave/events/events.jsonl

Compare that count with the runtime codon records for the run. If we find more completion records, retries or loop repetitions explain the difference.