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.
Read the diagram as text
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:
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:
| Field | Meaning |
|---|---|
id | A runtime-generated string identifier; it has no semantic prefix. |
type | A dot-separated event type literal, such as state.transition or codon.completed. |
timestamp | An ISO 8601 timestamp for the event. |
data | The 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:
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:
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.
{"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 tolimitevents newest first, together withtotalEventsandhasMore. A handshake requests this view usinghandshakeHistoryLimit, whose default is 50.getAllEvents()is an async generator that reads one line at a time throughreadline. 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:
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.
| id | category | journaled | sentinelRouted | payloadFields | receipts |
|---|---|---|---|---|---|
| archive.completed | server-state | true | true | codonId, archivedPaths | schemas/event-schemas.ts:668, schemas/event-schemas.ts:977, schemas/event-schemas.ts:1257, hankweave-runtime.ts:5465 |
| archive.partial | server-state | true | true | codonId, archivedPaths, failedPaths | schemas/event-schemas.ts:673, schemas/event-schemas.ts:978, schemas/event-schemas.ts:1258, hankweave-runtime.ts:5450 |
| assistant.action | agentic-backbone | true | true | codonId, 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.summary | server-state | true | true | ceiling, allocation, rows, totals | schemas/event-schemas.ts:724, schemas/event-schemas.ts:979, schemas/event-schemas.ts:1267, hankweave-runtime.ts:6478 |
| checkpoint.list | server-state | true | true | runId, checkpoints, currentBranch | schemas/event-schemas.ts:637, schemas/event-schemas.ts:968, schemas/event-schemas.ts:1250, hankweave-runtime.ts:4255 |
| codon.completed | server-state | true | true | codonId, 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.extended | server-state | true | true | codonId, codonName, extensionNumber, exhaustWithPrompt, cumulativeTokens, cumulativeCost | schemas/event-schemas.ts:565, schemas/event-schemas.ts:962, schemas/event-schemas.ts:1236, hankweave-runtime.ts:2946 |
| codon.started | server-state | true | true | codonId, 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 |
| error | server-state | true | true | message, 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.updated | agentic-backbone | true | true | path, filename, content, action | schemas/event-schemas.ts:587, schemas/event-schemas.ts:988, schemas/event-schemas.ts:1240, hankweave-runtime.ts:2357, hankweave-runtime.ts:3927 |
| filetree.updated | agentic-backbone | true | true | tree | schemas/event-schemas.ts:592, schemas/event-schemas.ts:989, schemas/event-schemas.ts:1241, hankweave-runtime.ts:3954 |
| history.batch | connection-state | false | false | events, hasMore | schemas/event-schemas.ts:688, schemas/event-schemas.ts:1012, schemas/event-schemas.ts:1260, hankweave-runtime.ts:1312 |
| incomplete.codon | connection-state | false | false | codonId, codonName, message | schemas/event-schemas.ts:622, schemas/event-schemas.ts:1013, schemas/event-schemas.ts:1247 |
| info | server-state | true | true | message | schemas/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.completed | server-state | true | true | loopId, 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 |
| pong | connection-state | false | false | message, 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.output | agentic-backbone | true | true | codonId, stream, line, commandIndex | schemas/event-schemas.ts:607, schemas/event-schemas.ts:992, schemas/event-schemas.ts:1244, hankweave-runtime.ts:6109 |
| rig.setup.completed | agentic-backbone | true | true | codonId, rigType, commandCount, durationMs, createdCheckpoint | schemas/event-schemas.ts:597, schemas/event-schemas.ts:990, schemas/event-schemas.ts:1242, hankweave-runtime.ts:2160 |
| rig.setup.failed | agentic-backbone | true | true | codonId, failureType, exitCode?, commandIndex?, ignored | schemas/event-schemas.ts:602, schemas/event-schemas.ts:991, schemas/event-schemas.ts:1243, hankweave-runtime.ts:1791 |
| rollback.archiveRestore | server-state | true | true | codonId, restoredPaths, failedPaths?, status | schemas/event-schemas.ts:678, schemas/event-schemas.ts:974, schemas/event-schemas.ts:1256, hankweave-runtime.ts:5557 |
| rollback.codonCheckpoint | server-state | true | true | codonId, codonName, checkpoint, checkpointType, message | schemas/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.completed | server-state | true | true | fromRun, toRun, checkpoint, codonId, codonName, checkpointType, autoRestart | schemas/event-schemas.ts:662, schemas/event-schemas.ts:972, schemas/event-schemas.ts:1255, hankweave-runtime.ts:4722, hankweave-runtime.ts:5254 |
| rollback.progress | server-state | true | true | currentStep, totalSteps, message | schemas/event-schemas.ts:657, schemas/event-schemas.ts:970, schemas/event-schemas.ts:1254, hankweave-runtime.ts:5117, hankweave-runtime.ts:5156 |
| rollback.rigCleanup | server-state | true | true | codonId, 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.started | server-state | true | true | fromRun, fromCodon, toCodon, toCheckpoint, checkpointType, codonsToProcess | schemas/event-schemas.ts:642, schemas/event-schemas.ts:969, schemas/event-schemas.ts:1251, hankweave-runtime.ts:4645, hankweave-runtime.ts:5095 |
| sentinel.error | sentinel | true | false | sentinelId, codonId, errorType, message, retriable, consecutiveFailureCount | schemas/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.loaded | sentinel | true | false | sentinelId, 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.output | sentinel | true | false | sentinelId, codonId, triggerNumber, outputType, content, cost, tokens, eventCount | schemas/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.triggered | sentinel | true | false | sentinelId, codonId, triggerNumber, strategy, eventCount, queueSize | schemas/event-schemas.ts:719, schemas/event-schemas.ts:1003, schemas/event-schemas.ts:1266, sentinels/sentinel.ts:392 |
| sentinel.unloaded | sentinel | true | false | sentinelId, codonId, reason, errorType?, finalCost, llmCallCount | schemas/event-schemas.ts:704, schemas/event-schemas.ts:1000, schemas/event-schemas.ts:1263, sentinels/sentinel-manager.ts:851 |
| server.idle | server-state | true | true | reason, message | schemas/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.ready | connection-state | false | false | serverVersion, 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.snapshot | server-state | true | true | currentCodon?, completedCodons, fileTree, totalCost, totalTime, recentFileAccess?, isRollingBack | schemas/event-schemas.ts:550, schemas/event-schemas.ts:963, schemas/event-schemas.ts:1233, hankweave-runtime.ts:1422 |
| state.transition | server-state | true | true | transitionType, runId?, codonId?, transition, resultingState | schemas/event-schemas.ts:693, schemas/event-schemas.ts:975, schemas/event-schemas.ts:1261, hankweave-runtime.ts:394 |
| token.usage | server-state | true | true | codonId, 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.result | agentic-backbone | true | true | codonId, toolUseId, toolName, result, truncated, originalLength, executionTimeMs, isError | schemas/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:
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 typedevent.data.failureReasonincludes its failure type andretriableflag. See errors and exit codes for the failure taxonomy. - Errors: select
event.type === "error". Itsdata.fatalfield is required;data.severity, when present, is one offatal,codon,operation, orwarning. - Time windows: compare the ISO 8601
timestamplexicographically. For example, this streams JSONL records into an array before filtering:
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:
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:
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:
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:
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.