Skip to content

feat(gui): add electron step log archiver for the marker protocol - #178

Draft
Emin017 wants to merge 27 commits into
mainfrom
emin/refactor-log-stream
Draft

feat(gui): add electron step log archiver for the marker protocol#178
Emin017 wants to merge 27 commits into
mainfrom
emin/refactor-log-stream

Conversation

@Emin017

@Emin017 Emin017 commented Aug 19, 2026

Copy link
Copy Markdown
Member

Summary

Scope

Select the areas touched by this PR:

  • GUI - desktop UI/runtime changes in ecos/gui, including renderer, Electron, and shared packages.
  • ECC - ECC submodule updates or ECOS Studio integration with the ECC CLI/runtime.
  • Resource management - resource registry, downloads, installation, manifests, PDKs, or tool assets.
  • Build, packaging, Nix, or release workflow - build inputs, AppImage packaging, or release metadata.
  • CI - GitHub Actions workflows, reusable actions, triggers, path filters, or automated checks.
  • Documentation only - README, guides, templates, or docs with no runtime behavior change.

Validation

List the commands you ran. Mark checks that are not applicable as N/A.

  • cd ecos/gui && pnpm run typecheck
  • cd ecos/gui && pnpm run test
  • cd ecos/gui && pnpm run build
  • make build
  • make demo-gcd
  • make demo-retrosoc
  • Manual GUI smoke: cd ecos/gui && pnpm run dev
  • Other:

Skipped checks and reason:

Screenshots or Recordings

Required for visible GUI changes.

Release, Packaging, and Runtime Impact

  • No release, packaging, or runtime impact
  • Version metadata changed
  • AppImage or Electron packaging changed
  • ECC CLI runtime resources changed
  • OSS CAD Suite, PDK, resource download, or installer behavior changed
  • Submodule gitlink changed

Notes:

Checklist

  • I kept the change scoped to the affected component.
  • I updated docs or user-facing text where behavior changed.
  • I included lockfile changes for dependency updates.
  • I documented intentional submodule updates.
  • I did not include local caches, virtual environments, or generated build outputs.
  • I explained any skipped validation and remaining risk.

Emin017 added 25 commits August 18, 2026 16:35
Signed-off-by: Emin <me@emin.chat>
The sidecar never writes step log files; it emits bytes and step
markers on stderr. The new StepLogArchiver consumes that stream in the
Electron process: a marker state machine ported from the normative
Python reader, per-step archives with truncate-on-begin, flow.json
allowlist, name sanitization plus symlink-aware workspace containment,
batched segments with byte-accurate cursors, a bounded per-step tail
for finalLog, and unscoped-byte routing. Disallowed or unsafe markers
degrade to ordinary bytes.
…chiver

Wire the sidecar's stderr through the step log archiver and add the
event bridge that owns operation context and crash-safe ordering:

- step.log events are synthesized with the active operation's identity
  (kind, origin, rerun, operationId, runSessionId, runtimeInstanceId,
  workspaceId), a minted eventId, and the most recently observed ecc
  sequence; ordering within a step is carried by payload.cursor.
- Segments for a step are buffered until its step.started has passed
  the tracker and fanout; buffer overflow drops only synthesis while
  the archive file stays complete.
- step.completed is held until the archiver's matching StepEnded, with
  a bounded timeout and sidecar close as cancellation sources; release
  flushes pending bytes and attaches the bounded finalLog tail.
- Unscoped bytes keep today's behavior exactly (sidecar log file plus
  runtime.stderr); scoped bytes no longer appear in either.
- The flow.json allowlist refreshes on workspace open, operation
  start, and rerun preparation. Synthesized events bypass the
  operation tracker by construction.
Electron-side integration tests drive notifications plus raw sidecar
stderr through the wired runtime: synthesized step.log events fan out
with workspace context and never touch the operation tracker, unscoped
bytes keep the sidecar log plus runtime.stderr path with no marker
leakage, and a held step.completed releases with finalLog before
runtime.exited when the sidecar dies mid-step.

Renderer-side tests pin the synthesized shape against the live event
mapping: a full-flow synthesized step.log still maps to cmd rtl2gds,
and repeated sequence values are accepted on synthesized events.
Review follow-ups for the electron step log synthesis:

- Segments are now buffered whenever the operation context or the
  step's step.started has not arrived yet, so stderr that races ahead
  of the RPC channel is released with the operation identity that
  actually produced it; stale buffers drop at terminal events and
  sidecar close instead of being misattributed.
- Skipped steps never emit markers, so their step.completed is
  forwarded immediately instead of waiting out the hold timeout and
  reordering the lifecycle; a superseded hold now logs a note when it
  releases.
- The archiver close path routes parser-held bytes (a truncated final
  line or marker candidate) into the archive or the unscoped stream
  instead of discarding them.
- Synthesized events track the most recently observed sequence rather
  than the maximum, and all lifecycle state resets on sidecar close.
- The bridge construction and protocol-event shaping move into
  runtimeProtocolFanout.ts, keeping workspaceRuntime.ts under 700
  lines.
Pin the ecc submodule to the branch that removes executor-side step
log writing and server-side tailing (marker protocol v1, end-marker
ordering, CLI worker unification, operations.py tail deletion). This
tree pairs the marker-producing ecc with the archiver-equipped GUI, so
checking out this PR always yields a consistent producer/consumer
pair; bumping ecc without the GUI archiver would break GUI step logs.
- The archiver keeps pending bytes and per-step tails as chunk lists
  with byte counters, replacing per-line whole-buffer concats with one
  concat per flush or tail read.
- Workspace containment reuses electron/services/pathScope's
  isPathWithinRoot instead of a local copy.
- The allowlist has a single key-based refresh entry point.
- Bump the ecc pin for the runtime/cli reuse cleanup.
Port the arbitrary-boundary scanning rule to the Electron archiver:
bytes before a candidate frame are ordinary data, trailing partial
prefixes are held back, incomplete candidates stay bounded, and only
valid newline-terminated v1 frames are consumed. Covers markers glued
to unterminated output, frames split across chunks, mid-line invalid
frames, and recovery after an overlong candidate.
A failed step does not wait for the render gate, so step.completed and
operation.failed can both reach the RPC channel before the stderr end
marker. The bridge now queues terminal events behind a held completion
and forwards them in lifecycle order once the hold releases. Hold
timeouts and superseded holds abandon the archiver's stale active
step, so the next begin starts a fresh attempt instead of reading as
nested, and every forwarded completion now consumes its attempt count
regardless of how it was released.
An activation-time open failure no longer installs step state: the
begin frame and the step's bytes route to the unscoped path (sidecar
log plus runtime.stderr) with a logged violation, so the output stays
visible. Tail-only abandonment remains reserved for mid-step write
failures. The in-memory tail now also slices oversized single chunks
on append, keeping its byte bound strict.
The synthesized-event mapping coverage (rtl2gds command mapping,
repeated-sequence tolerance) moves into its own file so the existing
contract suite remains an untouched regression net for AC-7.
ecc only admits a new operation after the previous one went terminal,
so a pending end marker can no longer arrive: the bridge now releases
the previous operation's held step.completed and forwards its queued
terminal event before the new operation.started, preserving lifecycle
order across the boundary instead of letting the terminal event
overtake the held completion.
Re-pin for the round-1 ecc fixes: arbitrary-boundary marker scanning
and RPC-error unmatched-step repair.
When the executor raises after begin, neither an end marker nor a
step.completed exists. The bridge now holds a terminal operation event
behind the archiver's StepEnded while an archive is still active: on a
matching end the terminal forwards immediately; on the bounded
timeout, sidecar close, or a superseding operation.started the stale
archive is flushed and abandoned first, so the next operation's begin
starts a fresh attempt and no late segment inherits its identity.
The operation tracker used to resolve terminal waiters before the step
log bridge's barrier released, so a new operation could start while
the stale archive was still active. Tracker, terminal lifecycle, and
fanout now live in one trackAndEmitProtocolEvent callback that runs
when the bridge forwards: synthesized step.log events stay emit-only,
step.started is tracked before buffered segments release, and a held
terminal keeps waitForOperation pending until StepEnded, timeout, or
close. Render-gate replays of step.completed forward immediately
instead of being re-held.
Move the shutdownBarrierFrom parser into runtimeClient.ts, which owns
the RPC shutdown result boundary, returning workspaceRuntime.ts to 691
lines with the tracker/barrier ownership unchanged. Split the bridge
test surface: shared harness and event factories live in
stepLogEventBridge.testSupport.ts, and the terminal/crash/supersession/
replay lifecycle cases move to stepLogEventBridge.lifecycle.test.ts —
every resulting module is under 700 lines with all assertions
preserved, plus a new guard test for render-gate replayed completions.
The holdback bound is now inclusive in the Electron archiver, matching
the specification and the Python reader: an incomplete candidate of
exactly 512 bytes is held until its newline arrives, while anything
longer degrades to ordinary bytes. Boundary tests cover both sides.
The Python reader rejects a marker whose payload is not valid UTF-8
(UnicodeDecodeError), but the TS archiver decoded lossy first, so an
invalid byte could still parse as a valid JSON marker. Switch
parseStepMarker to a fatal TextDecoder so a non-UTF-8 payload is
ordinary stream bytes on both consumers, and pin the parity with a
normative test.
Bumps the submodule past the two round-5 review fixes: the normative
non-UTF-8 marker rejection test and the executable-steps chaining
regression test.
The bridge cleared endedStepCounts when operation.started arrived, so a
StepEnded that raced ahead on stderr was erased and the later
step.completed waited out the full hold timeout. Replace the cumulative
ended/completed totals with per-key pending-end tokens: a matched
StepEnded adds one, a forwarded completion consumes one, and the
remaining tokens are cleared only when the previous operation's
terminal is actually forwarded (including timeout, close, and
supersession paths). The pre-operation race test now runs through
step.completed and asserts immediate forwarding with the complete
finalLog and no timer; a lifecycle regression proves a crashed
operation's leftover token cannot satisfy the next operation's
completion.
The operation.started supersession path for a held terminal cleared
only bufferedSegments, so a leftover pending-end token from the crashed
operation could satisfy the next operation's same-key completion before
its own end marker. Route the release through clearCorrelationState()
like every other terminal-forward path, with a regression covering
heldTerminal supersession followed by a same-key completion.
# Conflicts:
#	ecc
#	ecos/gui/apps/desktop-electron/electron/services/eccRpc/sidecarProcess.ts
Batch flushes (size threshold or window timer) can split a multibyte
UTF-8 code point; decoding each flush independently emitted replacement
glyphs into synthesized step.log text even though the archived bytes
were correct. Scoped segments now decode through a streaming
TextDecoder reset on each step activation, and unscoped text through
one finalized on close, so split characters reconstruct exactly.
Regressions pin both boundary cases with the archived bytes intact.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant