feat(streamer): Wave 1 DataChannel emitters (cursor + clipboard + file-upload) #1

Merged
triform-admin merged 11 commits from feat/wave-1-emitters into main 2026-05-07 02:42:16 +00:00

Summary

Adds the chromeless side of the Wave 1 DataChannel contract from
/workspace/.triform/guides/chromeless-datachannel-contract.md.
Three new RTCDataChannels open in the streamer-page offer SDP and
land their respective behaviours:

  • A1 — cursor (chromeless→portal): in-page probe injected via
    CDP Page.addScriptToEvaluateOnNewDocument listens to
    pointermove + getComputedStyle(...).cursor and fires through
    Runtime.addBinding. Drop-stale-on-coalesce backpressure: at most
    one outstanding payload, replaced rather than queued. Coordinates
    in page CSS pixels.
  • A2 — clipboard (bidirectional): page-side copy probe
    emits c2p envelopes with monotonic seq. Inbound p2c
    envelopes drop on stale seq (per contract §5 race resolution),
    drop on unsupported MIME (text/plain | text/html only), then apply
    via navigator.clipboard.writeText through CDP. Echo suppression
    on the inbound→outbound roundtrip. Counters are per-RTCPeerConnection.
  • A3 — file-upload (portal→chromeless): chunk reassembly per
    upload_id with 4-ahead reorder window, total/eof sanity rules,
    60 s orphan timeout, and name sanitisation (path separators / NUL
    / control chars / leading dots stripped, truncated to 255 bytes
    on UTF-8 boundary). On eof, lands the bytes via
    Runtime.evaluate + synthetic DataTransfer/File against the
    focused <input type=\"file\"> (falling back to first file input
    on the page). Returns \"ok\" / \"no-input\" / \"err:...\".

Plus an iteration script:

  • A0-fastpath — scripts/dev-push-streamer.sh: ~5-min round-trip
    (vs ~4-hour Chromium rebuild) for streamer.js-only iteration.
    Builds infra/Dockerfile, pushes to dev-<sha> tag, rolls
    the dev cluster's chromeless deployment. Refuses prod-shaped tags.

Design notes

CDP doesn't actually expose Page.setCursorChanged or
Browser.onClipboardChanged events as the original Wave 0 brief
suggested — the proven path is the Runtime.addBinding probe pattern
that capture/cursor-watcher and capture/clipboard-bridge already
use. We don't reuse those Go sidecars in-process because driving CDP
directly from the streamer-page is one fewer hop and avoids the
wire-format translation between sidecar internal envelopes and the
Wave 0 contract envelopes.

If the CDP attach itself fails (e.g. Chromium flag set blocks
WebSocket connections from the streamer-page to its own
:9222), all three DataChannels still get opened — the labels need
to land in the offer SDP so the portal's demux match doesn't fall
through to the other => log-and-drop arm. The emitters log the
install failure and stay dormant.

Test plan

  • In a Chromium 147 dev container with this image, observe that
    three additional DataChannels appear in pc.getStats()
    (data-channel reports with labels cursor, clipboard,
    file-upload).
  • Cursor: move the mouse over a chromeless page; confirm the
    portal-side B1 consumer logs incoming envelopes with kind per
    the CSS cursor of the element under the pointer.
  • Clipboard outbound: select + copy text in the headless page;
    confirm a c2p envelope arrives portal-side.
  • Clipboard inbound: portal sends a p2c envelope; confirm the
    in-container clipboard updates (verifiable via subsequent CDP
    Runtime.evaluate(navigator.clipboard.readText())).
  • File-upload happy path: portal sends a 200 KiB file (4 chunks
    of ≤64 KiB); confirm the focused <input type=\"file\"> receives
    the file and dispatches change.
  • File-upload orphan timeout: portal sends 1 chunk and stops;
    confirm a tracing::warn-shaped log line about orphan timeout
    appears 60 s later.
  • No regression on input / stats channels: existing
    tests/e2e/webrtc-drive-mode.spec.ts gates pass.
  • scripts/dev-push-streamer.sh --skip-push succeeds locally
    (smoke).

Constraint compliance

  • Existing input/stats channels untouched (only new code in
    start()'s data-channel block; no edits to the InputRelay /
    StatsRelay classes).
  • DataChannel labels exact: cursor, clipboard, file-upload
    (lowercase, hyphenated multi-word).
  • JSON envelopes match contract shape ({v:1, t, <field>}); binary
    payloads base64-encoded inside JSON, no binary frames.
  • Disposal-safety: defensive dc.readyState checks before send,
    paired close listeners, JSON.parse wrapped in try/catch, idempotent
    dispose() methods.

🤖 Generated with Claude Code

## Summary Adds the chromeless side of the Wave 1 DataChannel contract from `/workspace/.triform/guides/chromeless-datachannel-contract.md`. Three new RTCDataChannels open in the streamer-page offer SDP and land their respective behaviours: - **A1 — `cursor` (chromeless→portal)**: in-page probe injected via CDP `Page.addScriptToEvaluateOnNewDocument` listens to `pointermove` + `getComputedStyle(...).cursor` and fires through `Runtime.addBinding`. Drop-stale-on-coalesce backpressure: at most one outstanding payload, replaced rather than queued. Coordinates in page CSS pixels. - **A2 — `clipboard` (bidirectional)**: page-side `copy` probe emits `c2p` envelopes with monotonic `seq`. Inbound `p2c` envelopes drop on stale `seq` (per contract §5 race resolution), drop on unsupported MIME (text/plain | text/html only), then apply via `navigator.clipboard.writeText` through CDP. Echo suppression on the inbound→outbound roundtrip. Counters are per-RTCPeerConnection. - **A3 — `file-upload` (portal→chromeless)**: chunk reassembly per `upload_id` with 4-ahead reorder window, total/eof sanity rules, 60 s orphan timeout, and name sanitisation (path separators / NUL / control chars / leading dots stripped, truncated to 255 bytes on UTF-8 boundary). On `eof`, lands the bytes via `Runtime.evaluate` + synthetic DataTransfer/File against the focused `<input type=\"file\">` (falling back to first file input on the page). Returns `\"ok\"` / `\"no-input\"` / `\"err:...\"`. Plus an iteration script: - **A0-fastpath — `scripts/dev-push-streamer.sh`**: ~5-min round-trip (vs ~4-hour Chromium rebuild) for streamer.js-only iteration. Builds `infra/Dockerfile`, pushes to `dev-<sha>` tag, rolls the dev cluster's chromeless deployment. Refuses prod-shaped tags. ## Design notes CDP doesn't actually expose `Page.setCursorChanged` or `Browser.onClipboardChanged` events as the original Wave 0 brief suggested — the proven path is the Runtime.addBinding probe pattern that `capture/cursor-watcher` and `capture/clipboard-bridge` already use. We don't reuse those Go sidecars in-process because driving CDP directly from the streamer-page is one fewer hop and avoids the wire-format translation between sidecar internal envelopes and the Wave 0 contract envelopes. If the CDP attach itself fails (e.g. Chromium flag set blocks WebSocket connections from the streamer-page to its own `:9222`), all three DataChannels still get opened — the labels need to land in the offer SDP so the portal's demux match doesn't fall through to the `other =>` log-and-drop arm. The emitters log the install failure and stay dormant. ## Test plan - [ ] In a Chromium 147 dev container with this image, observe that three additional DataChannels appear in `pc.getStats()` (`data-channel` reports with labels `cursor`, `clipboard`, `file-upload`). - [ ] Cursor: move the mouse over a chromeless page; confirm the portal-side B1 consumer logs incoming envelopes with `kind` per the CSS cursor of the element under the pointer. - [ ] Clipboard outbound: select + copy text in the headless page; confirm a `c2p` envelope arrives portal-side. - [ ] Clipboard inbound: portal sends a `p2c` envelope; confirm the in-container clipboard updates (verifiable via subsequent CDP `Runtime.evaluate(navigator.clipboard.readText())`). - [ ] File-upload happy path: portal sends a 200 KiB file (4 chunks of ≤64 KiB); confirm the focused `<input type=\"file\">` receives the file and dispatches `change`. - [ ] File-upload orphan timeout: portal sends 1 chunk and stops; confirm a `tracing::warn`-shaped log line about orphan timeout appears 60 s later. - [ ] No regression on `input` / `stats` channels: existing `tests/e2e/webrtc-drive-mode.spec.ts` gates pass. - [ ] `scripts/dev-push-streamer.sh --skip-push` succeeds locally (smoke). ## Constraint compliance - Existing `input`/`stats` channels untouched (only new code in `start()`'s data-channel block; no edits to the `InputRelay` / `StatsRelay` classes). - DataChannel labels exact: `cursor`, `clipboard`, `file-upload` (lowercase, hyphenated multi-word). - JSON envelopes match contract shape (`{v:1, t, <field>}`); binary payloads base64-encoded inside JSON, no binary frames. - Disposal-safety: defensive `dc.readyState` checks before send, paired close listeners, JSON.parse wrapped in try/catch, idempotent `dispose()` methods. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adds the chromeless side of the cursor signal in the WebRTC
DataChannel contract under
/workspace/.triform/guides/chromeless-datachannel-contract.md §4.

* Opens a third DataChannel labelled exactly "cursor", ordered, in
  the same offer SDP as the existing input/stats channels. Label
  drift here silently routes to the portal demux's other =>
  log-and-drop arm, so it stays a literal string adjacent to the
  existing two createDataChannel calls.
* Adds a small CDPClient class (~/devtools/browser flat-mode) used
  by the emitter. The streamer.js page is served from
  http://localhost:9000 with the
  --unsafely-treat-insecure-origin-as-secure flag, which permits
  WebSocket dials to ws://localhost:9222.
* CDP doesn't actually expose a "Page.setCursorChanged" event (the
  brief was approximate) — the proven path matches the existing
  capture/cursor-watcher Go service: inject a small JS probe via
  Page.addScriptToEvaluateOnNewDocument that observes pointermove +
  getComputedStyle(...).cursor, calling back through a
  Runtime.addBinding-exposed function. Coordinates are page CSS
  pixels (clientX/clientY are CSS-px per the CSSOM spec, no DPR
  adjustment needed). Kind comes through as the CSS keyword.
* Drop-stale-on-coalesce backpressure: at most one outstanding
  pendingPayload; replaced rather than queued when the next change
  arrives. The dc.bufferedAmount > 16 KiB threshold pauses emit
  until the channel drains.
* If CDP attach fails the DataChannel still gets opened so the
  portal demux sees the label, but emitter stays dormant — the
  alternative (closing the channel) flips state on the portal side
  and is strictly worse.

Disposal: close listeners on both DC and CDP, idempotent dispose()
fires on teardown and channel close. JSON.parse paths in the CDP
event listener wrap in try/catch and silently drop malformed frames.

Contract: §4 (cursor), §2 (label exactness).
Adds the bidirectional clipboard DataChannel per
/workspace/.triform/guides/chromeless-datachannel-contract.md §5.

* Opens a fourth DataChannel labelled exactly "clipboard", ordered.
  Labels are case-sensitive — any drift silently demuxes to the
  portal's `other =>` arm.
* Reuses the CDPClient added in A1 (no duplication). Installs a
  page-side probe via Page.addScriptToEvaluateOnNewDocument that
  listens for "copy" events and reports captured selection text via
  Runtime.addBinding callback.
* Outbound (c2p): translates probe callbacks into the contract
  envelope `{v:1, t, clipboard:{dir:"c2p", seq, mime, data}}` and
  emits on the DC. `next_outbound_seq` increments per send. Initial
  Wave-1 scope is text/plain (text/html acceptance is wired but the
  emitter only produces text/plain for now — the contract permits
  both, so the portal receiver is forward-compat).
* Inbound (p2c): receive on DC, drop if `seq <= last_seen_inbound_seq`
  (per contract §5 race resolution), drop if MIME is outside
  text/plain | text/html (with DEBUG log), otherwise base64-decode
  data and apply via Runtime.evaluate(navigator.clipboard.writeText).
  Browser.grantPermissions is invoked at install time (with retry on
  the smaller permission set) so the writeText call doesn't trigger
  a UA prompt.
* Per-session counters: counters live for the RTCPeerConnection
  lifetime; on reconnect the page reloads (per existing teardown
  semantics) and both peers reset to seq=0 / last_seen=-1, which the
  contract calls out as harmless because the fresh DataChannel has
  no in-flight stale frames.
* Echo suppression: lastInboundText is remembered for one round so a
  page-side "copy" event that fires immediately after we apply an
  inbound write doesn't loop the same string back out as a c2p
  envelope.
* Disposal: dispose() detaches the CDP event listener and is
  invoked from teardown + dc.close. JSON.parse paths in onInbound
  wrap in try/catch.

Contract: §5 (clipboard), §2 (label exactness).
Adds the chromeless side of the file-upload DataChannel per
/workspace/.triform/guides/chromeless-datachannel-contract.md §6.

* Opens a fifth DataChannel labelled exactly "file-upload", ordered.
  This is the only one of the three new channels that's purely
  inbound on the chromeless side — the portal-side B3 producer pumps
  chunks; we just receive.
* Reassembly per upload_id: each chunk envelope carries
  {upload_id, seq, total, name, mime, eof, data}. We buffer chunks
  in a Map<upload_id, Map<seq, Uint8Array>> and slide an `expected`
  pointer past contiguous chunks. eof: true with expected >= total
  triggers completion.
* Validation per contract §6:
  1. Monotonic non-decreasing seq with up to 4-ahead reorder window
     (FILE_UPLOAD_REORDER_AHEAD = 4). Beyond that, the upload is
     dropped + warned.
  2. total constant across an upload_id; mismatch drops the upload.
  3. eof: true ⇔ seq == total - 1; any deviation drops the upload.
  4. name sanitised: path separators / NUL / 0x01-0x1F / 0x7F
     replaced with underscore, ".." sequences collapsed, leading
     dots stripped, truncated to 255 BYTES post-UTF-8 encode (with
     codepoint-boundary alignment so we never emit half a
     codepoint).
* 60 s orphan timeout: setTimeout per upload_id refreshes on each
  chunk; fires if no chunk arrives for 60 s and eof hasn't been
  reached. The buffer is discarded with one tracing::warn-level
  log line including {upload_id, name, received_chunks,
  expected_total}.
* Landing: completion uses Runtime.evaluate with a synthetic
  DataTransfer + File — the canonical CDP `DOM.setFileInputFiles`
  takes on-disk paths, not bytes, which would require an
  intermediate temp-file write. The Runtime.evaluate path locates
  the focused <input type=file> (falling back to the first file
  input on the page if none has focus), assigns input.files via
  DataTransfer, and dispatches synthetic `input` + `change` events
  so page handlers see the upload. Returns "ok" / "no-input" /
  "err:..." which we log accordingly. If no file input exists, the
  upload is dropped per the contract's known Wave-1 limitation.
* Disposal: dispose() walks remaining uploads and discards each
  (clearing its timer), invoked from teardown + dc.close.

Contract: §6 (file-upload), §2 (label exactness).
feat(scripts): dev-push-streamer.sh for ~5-min iteration (Wave 1 A0-fastpath)
Some checks failed
CI / Docs link check (pull_request) Failing after 11s
CI / Lint (pull_request) Failing after 13s
CodeQL / Analyze javascript-typescript (pull_request) Failing after 10s
CodeQL / Analyze go (pull_request) Failing after 16s
CI / Build container image (pull_request) Failing after 24s
CI / Container smoke test (pull_request) Has been skipped
E2E / docker-compose + Playwright (pull_request) Failing after 25s
91ee3bdba8
Adds an iteration script for streamer-page-only changes, keyed off the
A0 recon finding that infra/Dockerfile:139 just COPYs
capture/streamer-page/ into the runtime image — no Chromium rebuild
required. Total round-trip target: ~5 min (vs the ~4-hour cold build
when Chromium-from-source is in scope).

Usage:
  scripts/dev-push-streamer.sh                     # build + push + roll
  scripts/dev-push-streamer.sh --tag my-feature    # use dev-my-feature
  scripts/dev-push-streamer.sh --skip-rollout      # build + push only
  scripts/dev-push-streamer.sh --skip-push         # build only (smoke)

Defaults to:
  registry: registry.triform.cloud
  image:    chromeless/chromeless
  tag:      dev-<short-sha>[-dirty]
  ns:       chromeless
  deploy:   tries chromeless-browserless / cb-browserless / chromeless

Refuses tags that look prod-shaped (cr7727-sw, latest, prod, …) so a
typo can't clobber the operator-managed prod image.

Credential paths handled, in order of preference:
  1. REGISTRY_USER + REGISTRY_PASSWORD env vars (script does login,
     and logout at exit).
  2. Existing ~/.docker/config.json or `docker login` already done.
  3. Soft warning if neither — push will then fail loudly with the
     real registry error rather than this script second-guessing.

Preflights: docker daemon reachable, kubectl present + namespace
visible. Bails before kicking off the build if any of those are
missing so wasted time is minimised. Post-build sanity check
md5s the streamer.js inside the resulting image against the host
copy — catches the case where a cached layer got picked up over a
local edit.

The script intentionally only targets dev tags + the dev cluster.
Integration / triform.wtf rollouts continue to flow through the
operator-managed kaniko Job + Flux image-automation in the triform
monorepo, NOT this script.
Adds the chromeless-side observability emit path defined by the D4-YAML
schema at chemistry/elements/tools/chromeless/.triform/observability.yaml.

Endpoint: POST /webrtc-event (new; sibling to existing /stats-update so
the two pipelines stay decoupled — stats is a high-frequency relay
keyed on tenant/session, webrtc-event is low-frequency lifecycle
beacons keyed on element_id).

Wire shape: { event: "<dot.name>", attrs: {...} }. Each event maps to
a chromeless_webrtc_* counter increment, plus histogram observations
on session_closed (duration_ms) and dc.opened (handshake_ms).

Counters declared (names match the YAML's chromeless_* metrics 1:1
plus four chromeless-side debug counters for cursor coalescing,
clipboard MIME/seq drops, and file-upload outcomes):

  chromeless_webrtc_session_count{element_id}
  chromeless_webrtc_ice_connected_count{element_id}
  chromeless_webrtc_ice_failed_count{element_id}
  chromeless_webrtc_dc_opened_count{element_id, label}
  chromeless_webrtc_replay_hit_count{element_id}        (broker-side, accepted on principle)
  chromeless_webrtc_replay_miss_count{element_id}       (broker-side, accepted on principle)
  chromeless_webrtc_session_duration_ms{element_id}     (histogram)
  chromeless_webrtc_signaling_handshake_ms{element_id}  (histogram)
  chromeless_webrtc_dc_cursor_coalesced_count{element_id}
  chromeless_webrtc_dc_clipboard_unsupported_mime_count{element_id}
  chromeless_webrtc_dc_clipboard_stale_seq_count{element_id}
  chromeless_webrtc_dc_file_upload_orphan_timeout_count{element_id}
  chromeless_webrtc_dc_file_upload_completed_count{element_id, result}

element_id sourced from $CHROMELESS_ELEMENT_ID at process start;
defaults to "_unknown" so dev-compose smoke runs surface metrics even
without a concrete element binding. Series are pre-registered for
the five known channel labels (input|stats|cursor|clipboard|file-upload)
so dashboards bind on an idle pod.

Unknown event names accept silently (204) for forward-compat — a
streamer.js that emits a new event name doesn't fail deployment if
the sidecar hasn't been updated.

OTLP-export gap: the sidecar has an OTLP *trace* exporter (tracing.go)
but no OTLP *metrics* exporter today. Prometheus scrape covers the
D4-YAML metric series; Triform's event-bus consumption of the dot-
named events at the OTLP-logs layer (for activity feed enrichment) is
a future task. A4 caps at "events produced + counters incrementing in
/metrics".

Verification:
  go build ./...   passes
  go vet ./...     clean
  go test ./...    21/21 pass (12 existing + 9 new)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(streamer): emit webrtc events to metrics-sidecar (Wave 2 A4)
Some checks failed
CI / Lint (pull_request) Failing after 2s
CI / Docs link check (pull_request) Failing after 1s
CodeQL / Analyze javascript-typescript (pull_request) Failing after 2s
CodeQL / Analyze go (pull_request) Failing after 5s
CI / Build container image (pull_request) Failing after 9s
CI / Container smoke test (pull_request) Has been skipped
E2E / docker-compose + Playwright (pull_request) Failing after 13s
98502646e1
Wires the chromeless-side WebRTC lifecycle and DataChannel events
into POSTs against chromeless-metrics-sidecar's new /webrtc-event
endpoint, so the metric series declared in
chemistry/elements/tools/chromeless/.triform/observability.yaml
populate with chromeless-side data.

New: MetricsEmitter class (mirrors StatsRelay shape — fire-and-forget
fetch, rate-limited error log on bursts, "off" sentinel disables emit
entirely for tests). Module-scope singleton created on first start()
so the in-class emitters (CursorEmitter, ClipboardChannel,
FileUploadReceiver) can call metrics.emit() without constructor
threading.

Configuration: ?webrtc_metrics=<url> overrides the default
http://localhost:9100/webrtc-event. ?webrtc_metrics=off disables emit.

Lifecycle events emitted (per D4-YAML):

  chromeless.webrtc.session_created   PC connectionState first
                                      reaches `connected`. Latched
                                      so flap-back to connecting +
                                      re-connect doesn't double-emit.
  chromeless.webrtc.session_closed    On teardown, with duration_ms
                                      between session_created and
                                      teardown start. Latched.
  chromeless.webrtc.ice.connected     iceConnectionState in
                                      {connected, completed}. Cancels
                                      a pending ice.failed debounce.
  chromeless.webrtc.ice.failed        iceConnectionState in
                                      {failed, disconnected} that
                                      hasn't recovered after 3 s.
  chromeless.webrtc.dc.opened         Per-channel `open` event for
                                      input | stats | cursor |
                                      clipboard | file-upload. The
                                      first dc.opened of each PC
                                      carries handshake_ms (time
                                      from session_created or page
                                      bootstrap to first DC open).

DataChannel-internal counters:

  chromeless.webrtc.dc.cursor.coalesced
      In CursorEmitter.queue when pendingPayload was non-null
      before being replaced.
  chromeless.webrtc.dc.clipboard.unsupported_mime
      In ClipboardChannel.onInbound when MIME is neither text/plain
      nor text/html.
  chromeless.webrtc.dc.clipboard.stale_seq
      In ClipboardChannel.onInbound when seq <= lastSeenInboundSeq.
  chromeless.webrtc.dc.file_upload.orphan_timeout
      In FileUploadReceiver.armOrphanTimer when the 60 s timer fires
      without a completion.
  chromeless.webrtc.dc.file_upload.completed
      In FileUploadReceiver.complete on every terminal path
      (result = "ok" | "no-input" | "err").

Names are exact dot-form matches against the YAML schema and the
generated chemistry/generated/physics-gen/src/observability.rs
EventRule patterns.

Per-session state (sessionId via crypto.randomUUID, sessionStartedAt,
firstDCOpenedSeen, iceFailedDebounce) lives inside start()'s closure
so a future page-bootstrap reattach mints a fresh session pair. The
emitSessionClosed closure is exposed on `active` so the module-scope
teardown() can pair every session_created with exactly one
session_closed regardless of which teardown path fires.

Replay events (replay.hit, replay.miss) deliberately NOT emitted
here — those are broker-side concerns belonging to physics-side
instrumentation, flagged in the A4 followup. The sidecar's
/webrtc-event endpoint accepts them on principle so a future
physics emitter can route through any chromeless sidecar.

Verification: `node --check capture/streamer-page/streamer.js`
passes (all 1921 lines parse cleanly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the test sent three dc.opened events each carrying
handshake_ms: 250 and only asserted CollectAndCount > 0 on the
handshake histogram. That assertion silently passes for any
sample_count >= 1, so a regression that attached handshake_ms to
multiple dc.opened events per session would not be caught.

Per the YAML schema (webrtc_signaling_handshake_ms: "from
session_created to FIRST dc.opened") and the streamer's
firstDCOpenedSeen latch, only the first dc.opened of a session
carries handshake_ms. Model that discipline in the test:
   - first event (cursor) carries handshake_ms
   - subsequent events (clipboard, file-upload) do not

Then assert sample_count == 1, not "> 0". Adds a small
histSampleCount helper that reads the dto.Metric proto for the
specific label set, since testutil.CollectAndCount only counts
distinct series — not observations on a single series.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous guard was a hand-enumerated list:
   cr7727-sw|cr7727-aura*|latest|main|prod|stable

It missed every other cr7727-* shape (cr7727-release, cr7727-v2.1.0,
cr7727-rc1, ...), missed `release-*` and `prod-*` family tags, and
missed `main-<sha>` shapes.

The script's contract is "never push to a prod-shape tag" — not "never
push to a known prod tag literal". Switch to glob matching:
   cr7727-*|release*|prod*|main*|latest|stable

so future operator naming choices don't quietly fall through. Update
the error message to name the matched family, so a developer who
typos --tag knows which shape they hit.

Verified rejection of cr7727-sw, cr7727-aura, cr7727-v2.1.0,
cr7727-release, release-2026, prod-eu, main, latest, stable; verified
dev-myfeature still passes the guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(metrics-sidecar): session_closed counter for active-session panels (FU #34)
Some checks failed
CI / Lint (pull_request) Failing after 1s
CI / Docs link check (pull_request) Failing after 1s
CodeQL / Analyze javascript-typescript (pull_request) Failing after 2s
CodeQL / Analyze go (pull_request) Failing after 5s
CI / Build container image (pull_request) Failing after 9s
CI / Container smoke test (pull_request) Has been skipped
E2E / docker-compose + Playwright (pull_request) Failing after 14s
572aaf6f9b
D4-Dash's "Active sessions = created − closed" panel had no counter
to read for the "closed" half — session_closed events fed only the
duration histogram, which double-counts across pod restarts and
isn't a clean monotonic counter.

Add chromeless_webrtc_session_closed_count, dimensioned by
element_id (same shape as chromeless_webrtc_session_count). Bumps
unconditionally on session_closed, independent of whether
duration_ms is present — a closed session is closed regardless of
whether the emitter knew its duration.

Touch points:
   - webrtc_handler.go: declare counter, increment on session_closed
   - main.go: pre-register so the series exists on /metrics from
     startup, before the first session
   - webrtc_handler_test.go: extend SessionClosedDuration to assert
     the counter and tighten the histogram check via histSampleCount;
     add SessionClosedNoDurationStillCountsCounter for the
     no-duration path

Pairs with the matching YAML declaration in
chemistry/elements/tools/chromeless/.triform/observability.yaml
(separate commit in the monorepo because chromeless and chemistry
live in different repos).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: package phase2 streamer runtime
Some checks failed
CI / Lint (pull_request) Failing after 1s
CI / Docs link check (pull_request) Failing after 1s
CodeQL / Analyze javascript-typescript (pull_request) Failing after 2s
CodeQL / Analyze go (pull_request) Failing after 6s
CI / Build container image (pull_request) Failing after 9s
CI / Container smoke test (pull_request) Has been skipped
E2E / docker-compose + Playwright (pull_request) Failing after 15s
ffc3320f7e
fix: restore signaling lint baseline
Some checks failed
CI / Lint (pull_request) Failing after 2s
CI / Docs link check (pull_request) Failing after 2s
CodeQL / Analyze javascript-typescript (pull_request) Failing after 2s
CodeQL / Analyze go (pull_request) Failing after 5s
CI / Build container image (pull_request) Failing after 9s
CI / Container smoke test (pull_request) Has been skipped
E2E / docker-compose + Playwright (pull_request) Failing after 14s
3a90db37a0
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
triform/chromeless!1
No description provided.