Call journal
The call journal is a file that records every bridge call before it runs, so after a crash you can see which call was running. Read this to find the file, read it, and understand request ids, which stop a retried write from running twice.
Why the record is written first
An editor can die in the middle of a call, and without a record nobody can say which call did it. A journal written after a call completes describes every call except the one that killed the process, which is the only one anyone wanted. So the bridge writes a start record and flushes it to disk before the handler runs, and writes an end record after. At the next launch:
- a
startwith no matchingendmeans that endpoint was running when the process stopped; - a
sessionrecord with noshutdownmeans that editor died instead of being closed.
The diagnosis is an absence. An ordinary log cannot carry it, because log lines are handed to a background buffer that does not flush each line, which loses exactly the tail you need.
Where the files are
| Unreal | Blender | |
|---|---|---|
| File | Saved/MifBridge/journal.jsonl in the project |
%LOCALAPPDATA%\MifBridge\Blender\journal.jsonl on Windows. MIF_BLENDER_JOURNAL overrides the path |
| Scope | one file per project, appended across runs | one file per user, since Blender has no project and an unsaved .blend has no folder |
| Format | one JSON record per line | one JSON record per line, with the same field names plus pid and seq on every record |
| Size | grows | rolls over to journal.1.jsonl at 16 MB |
The records use the fields t (time), ev (the event: session, start, end, shutdown, replay), ep (the endpoint or op), ok, ms and err. Session records carry the process id, and so do start and end records, because two editors on one project write to the same file and their records interleave.
Two editors on one project both journal. The file is opened append-only and shared, so Windows puts every write at the true end of the file for every writer. In a test, 600 calls from two editors at once wrote 1,200 records, none torn, each under its own pid. The Blender journal uses the same method, since four processes appending through an ordinary append mode lost 55% of their lines and tore 2% in a measurement, and an append-only handle lost none.
Reading it
Unreal
python tools/mifwatch.py
It reports each recent session, its call count and its slowest call, and the useful part: any call that started and never returned. It ships with the plugin and reads the journal of the project the plugin is installed in, or of the running editor's project when one is up. --journal <file> reads another file.
A session with no shutdown record is LIVE when its process is still alive and is an Unreal editor, and its open call is then in flight. It is DIED otherwise: its process is gone, another program holds that process id, or a later session came up with the same one. A line torn mid-write is matched to the editor whose pid it still names.
--watch keeps the editor alive and relaunches it on death. Set MIF_EDITOR_EXE to your engine's UnrealEditor.exe for that. --watch --once relaunches at most once, then reports.
Blender
python tools/mifwatch.py --blender
Or inside an assistant session, call bl_call_log. It returns the last 64 calls from memory, or with disk: true reads the file and reports unfinished calls and sessions that never closed. The add-on preference Write the call journal to disk turns the file off and leaves the in-memory list running.
The Blender journal follows the same rule: the start record leaves the process before the op runs, so a start with no end is the call Blender died in. It is written, not forced to disk. That survives the process dying but not a power cut, and in exchange a call costs about 21 microseconds instead of a disk flush.
Bulk previews in the journal
A call that takes part in a bulk preview carries two more fields. A start record says "dryRun": true for a preview, or names the "previewId" being committed, so a commit that dies mid-write names its preview. The matching end record carries:
"bulk": {mode, endpoint, previewId, outcome, changes, targets[]}
outcome is previewed, committed, drift, rolled-back, rollback-incomplete or a refusal. targets lists up to 100 of the actors, rows and objects the call touched, which is the record of which assets a bulk change affected.
Request ids: a call whose reply never came
Every Unreal endpoint runs on the game thread, so a caller whose read timed out cannot tell a call that never ran from one whose reply is late. A compile, a cook or a long import holds the reply while the work completes. If a caller simply retried a write the editor had already applied, it would run twice.
The contract
A call may carry an X-Mif-Request-Id header of 1 to 64 characters: letters, digits and - _ . :. Anything else is refused with HTTP 400 before anything runs. It is a header and not a body key, because requestId is already a real parameter of two Game Framework endpoints. Before dispatch the id is recorded as running, and after the handler its reply is stored. A second request with the same id is answered from the record and runs nothing:
| The record | The answer |
|---|---|
| done, reply kept | the first reply, plus replayed: true, requestId, firstRunSecondsAgo |
| done, reply not kept (over 1,048,576 characters, or dropped for room) | ok:false, alreadyRan: true, firstOk, the first error, replayed: true. Read the target to see what happened |
| still running | ok:false, requestRunning: true, retryable: true |
| same id, another endpoint or body | ok:false, requestIdConflict: true, firstEndpoint. Nothing ran |
request_status reads a record. It takes requestId and optionally includeResult, and answers with a state:
running, withseconds;done, withendpoint,startedAt,endedAt,seconds,resultOk,resultError,responseKeptandresponseChars. WithincludeResultit adds the kept reply asresult;unknown, meaning the call never reached this editor, finished over 600 seconds ago, or the editor restarted.
request_status is a read, and it also answers while a modal dialog is up.
Retention
Records live in memory for 600 seconds after a call ends, at most 1,024 of them, and replies are dropped oldest first past 16,777,216 characters in all. An id is a retry handle, not a history. The journal's start record carries it as "rid", so after a crash the start with no end still names the id, and a replay writes one {"ev":"replay","rid":...} line so the file shows the repeat ran nothing.
A refusal by the modal-dialog guard is not remembered. It was answered before anything ran, so replaying it after the dialog is answered would refuse a call that can now run. The id is forgotten and a repeat runs.
What the MCP server does for you
The MCP server sends a fresh id on every Unreal call. After a read timeout it remembers the call, so the identical call made again within 600 seconds carries the same id and replays. Any answer makes it forget the call. The timeout reply names requestId, the endpoint's bucket, and checkWith (request_status, called through mif_call).
A call that may write answers outcomeUnknown: true, retryable: false. That covers a transacted or self-managed endpoint, one on the write gate's list (writeGated in self_audit), and one whose bucket the server has not read. A read stays retryable: true.
This does not cover a call made with a different body. That is a new call, so a caller who "fixes" a timed-out write by changing it gets no protection, and the reply says not to. A raw HTTP caller that sends no id keeps the old behavior.
Cost
The journal flushes to disk twice per bridge call, once for the start record and once for the end. The flush is nearly all of the cost, about 2 ms a record flushed against 2 to 4 microseconds for the write alone, measured on one machine. The console variable mif.BridgeJournal turns it off. It defaults to on, deliberately: a crash journal that must be enabled before the crash is off when it matters.
If the operating system refuses a write or a flush, the log says so once with the Windows error. On Windows the file is opened with the long-path prefix when its path reaches 260 characters, so a deeply nested project still journals without long-path support enabled.
Related
The journal is the other half of the safety gate: the gate stops what it can, and the journal tells you what ran. The editor panel shows the same calls live in its ACTIVITY tab. A workflow gives every call it makes a request id of the form wf-<run>-<step>-a<attempt>, which appears here as rid.