You're reading the 0.10.0 archive.

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 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: server.ready, pong, history.batch, and incomplete.codon are sent to specific WebSocket clients and are not written to the permanent journal. If one is passed to EventJournal.append(), the method throws instead of journaling it. In the 0.10.0 catalog, 32 of 36 event types are 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.

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. The shipped .gitignore excludes .hankweave/events/, so the journal is not committed to the project. 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:

⌁ 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.

The excerpt below is a real nine-record capture used by the published-package schema consumer, from a run whose single codon summarized a meeting-notes file. Read it as a worked example of the categories above: the run 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; a complete journal also includes any other journaled categories and events emitted by that execution.

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. It keeps no separate index or sidecar file. 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 10% of the limit (or the excess, whichever is larger), 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(), getTotalEvents(), and createReadStream(). 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 and hasMore. A handshake requests this view using handshakeHistoryLimit, whose default is 50.
  • getAllEvents() is an async generator that reads one line at a time through readline. Use this for a large journal instead of loading the whole file into memory.
  • getTotalEvents() returns the backend's in-memory count. File storage computes it during initialization and increments it on append.
  • streamAllEvents() exposes the raw readable stream, which can be piped to another process, gzip, or an HTTP response.

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 fire-and-forget CLI telemetry; that subscription is separate from journal storage.

For a small file, the same JSONL shape can be loaded into an array. For a large file, use the streaming form instead:

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

The journal is append-only: records are not modified, deleted, or reordered after they are written. The reader does not skip malformed records. If a non-empty line is not valid JSON, JSON.parse in getAllEvents() throws, so validate or repair the file before treating a bulk read as complete.

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 0.10.0 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:668, schemas/event-schemas.ts:977, schemas/event-schemas.ts:1257, hankweave-runtime.ts:5465
archive.partialserver-statetruetruecodonId, archivedPaths, failedPathsschemas/event-schemas.ts:673, schemas/event-schemas.ts:978, schemas/event-schemas.ts:1258, hankweave-runtime.ts:5450
assistant.actionagentic-backbonetruetruecodonId, action, content, toolName?, toolInput?schemas/event-schemas.ts:572, schemas/event-schemas.ts:986, schemas/event-schemas.ts:1237, hankweave-runtime.ts:2777, hankweave-runtime.ts:2789, hankweave-runtime.ts:2824
budget.summaryserver-statetruetrueceiling, allocation, rows, totalsschemas/event-schemas.ts:724, schemas/event-schemas.ts:979, schemas/event-schemas.ts:1267, hankweave-runtime.ts:6478
checkpoint.listserver-statetruetruerunId, checkpoints, currentBranchschemas/event-schemas.ts:637, schemas/event-schemas.ts:968, schemas/event-schemas.ts:1250, hankweave-runtime.ts:4255
codon.completedserver-statetruetruecodonId, success, cost, duration, exitStatus, failureReason?, failureIgnored?, budgetExceeded?schemas/event-schemas.ts:560, schemas/event-schemas.ts:961, schemas/event-schemas.ts:1235, hankweave-runtime.ts:1886, hankweave-runtime.ts:2064, hankweave-runtime.ts:2253, hankweave-runtime.ts:3365
codon.extendedserver-statetruetruecodonId, codonName, extensionNumber, exhaustWithPrompt, cumulativeTokens, cumulativeCostschemas/event-schemas.ts:565, schemas/event-schemas.ts:962, schemas/event-schemas.ts:1236, hankweave-runtime.ts:2946
codon.startedserver-statetruetruecodonId, codonName, codonDescription?, sessionId, previousSessionId?, startTime, promptMetadata?schemas/event-schemas.ts:555, schemas/event-schemas.ts:960, schemas/event-schemas.ts:1234, hankweave-runtime.ts:316
errorserver-statetruetruemessage, codon?, fatal, severity?, context?, code?schemas/event-schemas.ts:617, schemas/event-schemas.ts:967, schemas/event-schemas.ts:1246, hankweave-runtime.ts:975, hankweave-runtime.ts:995, hankweave-runtime.ts:1019, hankweave-runtime.ts:1062, hankweave-runtime.ts:1090, hankweave-runtime.ts:1116, hankweave-runtime.ts:1814, hankweave-runtime.ts:1867, hankweave-runtime.ts:2045, hankweave-runtime.ts:2236, hankweave-runtime.ts:2609, hankweave-runtime.ts:3981, hankweave-runtime.ts:4177, hankweave-runtime.ts:4273, hankweave-runtime.ts:4349, hankweave-runtime.ts:4512, hankweave-runtime.ts:4536, hankweave-runtime.ts:4599, hankweave-runtime.ts:4769, hankweave-runtime.ts:4804, hankweave-runtime.ts:4896, hankweave-runtime.ts:4918, hankweave-runtime.ts:5016
file.updatedagentic-backbonetruetruepath, filename, content, actionschemas/event-schemas.ts:587, schemas/event-schemas.ts:988, schemas/event-schemas.ts:1240, hankweave-runtime.ts:2357, hankweave-runtime.ts:3927
filetree.updatedagentic-backbonetruetruetreeschemas/event-schemas.ts:592, schemas/event-schemas.ts:989, schemas/event-schemas.ts:1241, hankweave-runtime.ts:3954
history.batchconnection-statefalsefalseevents, hasMoreschemas/event-schemas.ts:688, schemas/event-schemas.ts:1012, schemas/event-schemas.ts:1260, hankweave-runtime.ts:1312
incomplete.codonconnection-statefalsefalsecodonId, codonName, messageschemas/event-schemas.ts:622, schemas/event-schemas.ts:1013, schemas/event-schemas.ts:1247
infoserver-statetruetruemessageschemas/event-schemas.ts:627, schemas/event-schemas.ts:966, schemas/event-schemas.ts:1248, hankweave-runtime.ts:1705, hankweave-runtime.ts:1728, hankweave-runtime.ts:1902, hankweave-runtime.ts:1933, hankweave-runtime.ts:1971, hankweave-runtime.ts:2080, hankweave-runtime.ts:2196, hankweave-runtime.ts:2269, hankweave-runtime.ts:2730, hankweave-runtime.ts:3082, hankweave-runtime.ts:3100, hankweave-runtime.ts:3181, hankweave-runtime.ts:3205, hankweave-runtime.ts:3474, hankweave-runtime.ts:3723, hankweave-runtime.ts:3755, hankweave-runtime.ts:4058, hankweave-runtime.ts:4328, hankweave-runtime.ts:6427
loop.iteration.completedserver-statetruetrueloopId, iteration, durationMs, costUsd, tokensUsed, isFinal, terminationReason?schemas/event-schemas.ts:612, schemas/event-schemas.ts:976, schemas/event-schemas.ts:1245, hankweave-runtime.ts:6035
pongconnection-statefalsefalsemessage, timestamp, clientId?schemas/event-schemas.ts:683, schemas/event-schemas.ts:1011, schemas/event-schemas.ts:1259, hankweave-runtime.ts:1215, hankweave-runtime.ts:1243
rig.outputagentic-backbonetruetruecodonId, stream, line, commandIndexschemas/event-schemas.ts:607, schemas/event-schemas.ts:992, schemas/event-schemas.ts:1244, hankweave-runtime.ts:6109
rig.setup.completedagentic-backbonetruetruecodonId, rigType, commandCount, durationMs, createdCheckpointschemas/event-schemas.ts:597, schemas/event-schemas.ts:990, schemas/event-schemas.ts:1242, hankweave-runtime.ts:2160
rig.setup.failedagentic-backbonetruetruecodonId, failureType, exitCode?, commandIndex?, ignoredschemas/event-schemas.ts:602, schemas/event-schemas.ts:991, schemas/event-schemas.ts:1243, hankweave-runtime.ts:1791
rollback.archiveRestoreserver-statetruetruecodonId, restoredPaths, failedPaths?, statusschemas/event-schemas.ts:678, schemas/event-schemas.ts:974, schemas/event-schemas.ts:1256, hankweave-runtime.ts:5557
rollback.codonCheckpointserver-statetruetruecodonId, codonName, checkpoint, checkpointType, messageschemas/event-schemas.ts:647, schemas/event-schemas.ts:971, schemas/event-schemas.ts:1252, hankweave-runtime.ts:4684, hankweave-runtime.ts:5136, hankweave-runtime.ts:5191
rollback.completedserver-statetruetruefromRun, toRun, checkpoint, codonId, codonName, checkpointType, autoRestartschemas/event-schemas.ts:662, schemas/event-schemas.ts:972, schemas/event-schemas.ts:1255, hankweave-runtime.ts:4722, hankweave-runtime.ts:5254
rollback.progressserver-statetruetruecurrentStep, totalSteps, messageschemas/event-schemas.ts:657, schemas/event-schemas.ts:970, schemas/event-schemas.ts:1254, hankweave-runtime.ts:5117, hankweave-runtime.ts:5156
rollback.rigCleanupserver-statetruetruecodonId, codonName, directories, status, successfulCleanups?, failedCleanups?, error?schemas/event-schemas.ts:652, schemas/event-schemas.ts:973, schemas/event-schemas.ts:1253, hankweave-runtime.ts:5624, hankweave-runtime.ts:5662, hankweave-runtime.ts:5678
rollback.startedserver-statetruetruefromRun, fromCodon, toCodon, toCheckpoint, checkpointType, codonsToProcessschemas/event-schemas.ts:642, schemas/event-schemas.ts:969, schemas/event-schemas.ts:1251, hankweave-runtime.ts:4645, hankweave-runtime.ts:5095
sentinel.errorsentineltruefalsesentinelId, codonId, errorType, message, retriable, consecutiveFailureCountschemas/event-schemas.ts:709, schemas/event-schemas.ts:1001, schemas/event-schemas.ts:1264, sentinels/sentinel.ts:576, sentinels/sentinel.ts:663, sentinels/sentinel.ts:798, sentinels/sentinel.ts:905
sentinel.loadedsentineltruefalsesentinelId, codonId, model, triggerType, executionStrategy, conversational, source, sourcePath?schemas/event-schemas.ts:699, schemas/event-schemas.ts:999, schemas/event-schemas.ts:1262, hankweave-runtime.ts:5819
sentinel.outputsentineltruefalsesentinelId, codonId, triggerNumber, outputType, content, cost, tokens, eventCountschemas/event-schemas.ts:714, schemas/event-schemas.ts:1002, schemas/event-schemas.ts:1265, sentinels/sentinel.ts:538, sentinels/sentinel.ts:635, sentinels/sentinel.ts:762, sentinels/sentinel.ts:877
sentinel.triggeredsentineltruefalsesentinelId, codonId, triggerNumber, strategy, eventCount, queueSizeschemas/event-schemas.ts:719, schemas/event-schemas.ts:1003, schemas/event-schemas.ts:1266, sentinels/sentinel.ts:392
sentinel.unloadedsentineltruefalsesentinelId, codonId, reason, errorType?, finalCost, llmCallCountschemas/event-schemas.ts:704, schemas/event-schemas.ts:1000, schemas/event-schemas.ts:1263, sentinels/sentinel-manager.ts:851
server.idleserver-statetruetruereason, messageschemas/event-schemas.ts:632, schemas/event-schemas.ts:964, schemas/event-schemas.ts:1249, hankweave-runtime.ts:941, hankweave-runtime.ts:3621, hankweave-runtime.ts:3791, hankweave-runtime.ts:3812, hankweave-runtime.ts:4072, hankweave-runtime.ts:4743, hankweave-runtime.ts:5279, hankweave-runtime.ts:5291
server.readyconnection-statefalsefalseserverVersion, executionPath, agentRootPath, dataPath, port, proxyPort?, outputDirectory?schemas/event-schemas.ts:545, schemas/event-schemas.ts:1010, schemas/event-schemas.ts:1232, hankweave-runtime.ts:916
state.snapshotserver-statetruetruecurrentCodon?, completedCodons, fileTree, totalCost, totalTime, recentFileAccess?, isRollingBackschemas/event-schemas.ts:550, schemas/event-schemas.ts:963, schemas/event-schemas.ts:1233, hankweave-runtime.ts:1422
state.transitionserver-statetruetruetransitionType, runId?, codonId?, transition, resultingStateschemas/event-schemas.ts:693, schemas/event-schemas.ts:975, schemas/event-schemas.ts:1261, hankweave-runtime.ts:394
token.usageserver-statetruetruecodonId, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, totalCost, modelId?, modelUsage?schemas/event-schemas.ts:577, schemas/event-schemas.ts:965, schemas/event-schemas.ts:1238, hankweave-runtime.ts:2646, hankweave-runtime.ts:2660
tool.resultagentic-backbonetruetruecodonId, toolUseId, toolName, result, truncated, originalLength, executionTimeMs, isErrorschemas/event-schemas.ts:582, schemas/event-schemas.ts:987, schemas/event-schemas.ts:1239, hankweave-runtime.ts:2906

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:
⌁ 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. Because codon.completed carries codonId but not runId, assign each completion to the enclosing state.transition RunStarted/RunCompleted span, whose records carry runId. For a reported per-codon view, retain the final codon.completed for each (runId, codonId) and do not add token.usage.totalCost or retry totals on top. codon.completed.cost already includes accumulated retry cost. token.usage mixes incremental per-message costs with a final total, and modelUsage can be absent even on that final record; summing it can double-count.

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.

The configuration's default WebSocket-log path is .hankweave/logs/websocket.log. Its intended entry shape has loggedAt, direction (in or out), message, and optional metadata.size. In 0.10.0, however, the writer is reachable only through a deprecated wrapper that has no callers, so no runtime code path writes that file; the path and reader utility are not a usable wire-traffic record for this version. Do not substitute it for the event journal or promise that it contains a replayable WebSocket transcript.

When the journal grows too large#

The event journal has no automatic rotation, archive, or truncation. It grows monotonically for the life of the execution directory. 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.

Between runs, preserve history by moving or compressing events.jsonl. If the file is absent at the next execution, file-storage initialization recreates an empty one:

⌁ 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. 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.

Reading from disk with real import paths#

The published library surfaces for event parsing are under hankweave/schemas, not @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, as the server's own reader does:

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 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 produced seven completion events.

When summarizing spend or status, keep run boundaries from state.transition and retain the last completion for each (runId, codonId). 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:

⌁ 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.