Add an .engine RuntimeEvent source so host-tier issues
(devices, packages, attach, spawn-gating, instruments,
hookpacks, repl, itrace, editor) flow into the existing
event stream instead of stdout. HookPackLibrary takes a
callback for decode errors, and ITraceDecoder returns
collected panic strings so ProcessNode can yield them as
engine events.
Length-prefixed JSON log at events.log next to db.sqlite,
configurable byte cap with rolling compaction. Replays on
Engine.start.
toLogJSON/fromLogJSON carry the .raw payload and sessionID
that wire JSON drops. EventLog flushes a pendingPersist
queue so the visible-window trim can't drop events.
Engine drives device.enableSpawnGating() per armed-session intent;
matched spawns re-attach via the existing attach path and misses
resume immediately. Engagement intent is in-memory only so launches
don't auto-engage gating for sessions armed in a previous run.
ProcessSession gains armingState and lastArmPattern, mirrored in
the SQLite schema. RuntimeEvent.Source.spawnGating carries the
captured/released outcome with wire encoding.
Lab protocol adds update-arming as a SessionOp variant; armed-only
sessions announce themselves with pid 0 and phase detached so peers
can see them before any capture.
Engine.onUserNotification surfaces gating-enable failures using the
clean Frida.Error message.
Sessions in a lab are now collaborative artifacts. Any owner can
host (own the local Frida attachment) and any owner can drive
(issue REPL evals and instrument commands); a driver's actions
are relayed to the current host. Members watch read-only and see
an "Only lab owners can host sessions" CTA when they try the new
session button.
Re-host (Run on My Device…) lets an owner take over a running
session by attaching to their own equivalent process; the prior
host's Frida attachment detaches and the new host auto-loads the
session's instruments. Driver auto-reverts to host when the
current driver fully disconnects from the lab.
Workspace is now a thin host adapter. Engine owns sessions and
notebook entries (via store observation), GitHub auth + OAuth
device flow, hook-pack discovery, address annotations and address
actions, the disassembler cache, and all instrument lifecycle.
EventLog is @Observable so SwiftUI tracks it directly; the host
no longer mirrors event arrays or version counters.
R2 (live disassembly + trace inspection) lives entirely in
LumaCore as concrete Disassembler / TraceDisassembler classes
returning portable StyledText spans. ProcessNodeViewModel and
InstrumentRuntime are gone; views read engine.processNodes /
node.instruments / engine.descriptor(for:) directly.
Reorganize Luma/Models/ — instrument UI dispatch lives under
Luma/Instruments/, Monaco editor glue under Luma/Editor/, and the
remaining UI types sit at the top of Luma/.
ProcessNode is now in LumaCore, exposing AsyncStream for
events, REPL results, ITrace captures, module snapshots,
and session detach notifications. No delegates, no
@Published — consumers subscribe with for-await.
Luma retains ProcessNodeViewModel as a thin ObservableObject
wrapper that subscribes to the streams and handles SwiftData
persistence (inserting REPLCells, ITraceCaptures) and R2
integration.
RuntimeEvent is now a self-contained LumaCore value type
with source and payload enums instead of object references.
The Luma-side DisplayEvent bridges to views by carrying a
ProcessNodeViewModel reference alongside the core event.
WIP: views not yet updated for the rename — the Xcode build
needs mechanical updates across ~14 files to use
ProcessNodeViewModel and DisplayEvent.