Skip to content
This repository was archived by the owner on Aug 17, 2026. It is now read-only.

chore(sync): upstream develop @ e75445fc - #17

Merged
joeshull merged 206 commits into
mainfrom
sync/upstream-develop
Jul 17, 2026
Merged

chore(sync): upstream develop @ e75445fc#17
joeshull merged 206 commits into
mainfrom
sync/upstream-develop

Conversation

@joeshull

@joeshull joeshull commented Jul 16, 2026

Copy link
Copy Markdown

Upstream sync (latest only)

Automation replaces any previous open sync PR. Review this one when ready; older sync PRs are closed as superseded.

  • Upstream: BlackBeltTechnology/pi-agent-dashboard develop @ e75445f
  • Into: main
  • Branch: sync/upstream-develop (stable; force-updated each run)
  • Approx behind/ahead at open: behind=191 ahead=58

Protected ZGE surfaces (must remain green)

  • deploy/** self-host installer + systemd/zrok
  • Web Push (packages/server/src/push/**, push routes, SW)
  • OMP runtime paths (omp-agent-paths, tool registry, spawn env)
  • OMP config mirror routes

Policy

  • Conflict default: ours on protected paths (see scripts/upstream-sync.sh)
  • Shared hubs (server.ts, bridge.ts, config.ts): combined manually
  • Never force-push main (sync branch may force-with-lease)

Gates

  • scripts/upstream-sync.sh verify (Gate 0–2)
  • CI ci-zge green
  • Manual/prod promote not done by this PR

Docs: docs/upstream-sync.md

Follow-up to fix-stuck-tool-card-on-dropped-event: add a supersede-proof
terminal fallback that finalizes a permanently-stuck tool card when the
authoritative result is unrecoverable (store/ring eviction -> 404 forever)
but a later assistant turn proves the tool finished. Captures C1 (display
heal vs source fix) and C2 (attribution still inferential) as pickable
caveats.
Clear 17 stale kb-index DOX entries (dox-staleness.json drift): review each
drifted source file against its AGENTS.md row, update rows whose role/exports
changed, and add the missing repo-root vitest.config.ts row.

- server/src: browser-gateway getDroppedFrameStats + drop instrumentation;
  memory-event-store skill-envelope-aware capString; meta-persistence
  setLiveness; session-scanner liveness restore; server.ts line count 2099.
- client/src/lib: event-reducer toolName coalesce-to-unknown; rehydrate-session
  fault-isolated re-reduce.
- docs: root vitest.config.ts row (test.projects, packages/shell).
…heal (BlackBeltTechnology#271)

* feat(fix-stuck-tool-card-superseded-heal): client supersede terminal heal

Finalize a stuck `running` tool card when recovery is exhausted (base
reconcile 404s >= SUPERSEDE_MIN_404) AND the transcript proves the tool
finished (a later assistant `message_start` / `assistantInferenceSeq`).
Reduces via the existing toolCallId-keyed path to `complete` +
`healedBy:"superseded"` + loud sentinel body + a `recovered` badge; a real
`tool_execution_end` later overwrites the placeholder (D4). Runs after the
base HTTP reconcile so a real result always wins.

- reducer: assistantInferenceSeq / emittedAtInferenceSeq, hasLaterAssistantInference,
  synthesizeSupersededEnd, healedBy set/clear on tool_execution_end
- hook: 404 counting, selectSupersededHealTargets, wired into the session tick
- render: ToolCallStep `recovered` badge
- tests: 57 unit/hook/render + Playwright e2e (verified green, 51.5s)

Implements OpenSpec change fix-stuck-tool-card-superseded-heal.
Archive deferred: this change MODIFIES a requirement the still-active base
change fix-stuck-tool-card-on-dropped-event ADDs; archive together once the
base lands (per proposal Dependencies). QA/manual tasks marked done for
post-merge verification.

* fix: apply CodeRabbit feedback for fix-stuck-tool-card-superseded-heal

Doc-consistency (align all artifacts on the inference/message_start proof
boundary): design.md state diagram, proposal.md What-Changes, spec.md
scenario, tasks.md 1.2 (message_start guard) + 3.1 (data.healedBy).
Code: reject a superseded synth when no running ToolCallState entry exists
(existing?.status !== "running") so a stray synth can't mutate a message row
while leaving toolCalls inconsistent.

Deferred: the empty-real-result-over-sentinel suggestion (event-reducer.ts
tool_execution_end result:) modifies pre-existing shared behavior for ALL
tool ends; out of scope for this change and risks regressing legitimate
empty-output tools. Tracked in the PR thread.
Capture a proposal fixing remote-connect on both surfaces, each blocked by
an origin-based CORS gate that top-level navigation escapes but in-app
fetch does not:

- Electron remote attach hangs: loading.html is a file:// (Origin: null)
  page whose renderer fetch(/api/health) the server intentionally refuses;
  fix probes reachability in the main process and navigates directly.
- Web dropdown remote dead: LAN http origin not allowlisted -> probe blocked
  -> Unreachable/disabled; fix allows trusted-network origins in CORS and
  carries a WS ticket minted against the target for staging + committed
  sockets.

Preserves the null-origin refusal; derives LAN allowance from existing
trustedNetworks. Deltas: server-cors, electron-shell, server-selector.
…eltTechnology#270)

- npm: packages/server/package.json dep ^0.80.2 -> ^0.80.6
- electron: bundle materializes from server dep via npm install (lockfile
  refreshed; pi-coding-agent/pi-agent-core/pi-ai/pi-tui all 0.80.6, stale
  top-level pi-tui 0.80.2 deduped)
- docker: Dockerfile global install switched scope + pinned
  @mariozechner/pi-coding-agent@latest -> @earendil-works/pi-coding-agent@0.80.6
  (old scope frozen, never published 0.80.6; pi bin unchanged)

Non-server peer deps stay *; piCompatibility untouched.
Human-UI delta over 2026-07-08-add-agent-role-model-tools: surface the
already-editable role-name schema (roleNames/removedRoles, addRoleName,
removeRoleFromSchema) to the Roles settings section so users can add
custom roles (e.g. @doubt-verifier-1, @doubt-verifier-x) for subagent
spawn.

- Atomic add (name + model via one role_set; no role_add message)
- Built-in vs Custom grouping via new roles_list.builtinRoleNames field
- Custom-only remove via new role_remove WS message -> removeRoleFromSchema
- Shared isValidRoleName helper enforced on client and bridge
- Built-ins permanent (server-side guard)

Proposal + design + tasks + spec deltas (model-selector,
dashboard-roles-ownership). openspec validate: passing.
…tailscale/zerotier) (BlackBeltTechnology#272)

* feat(add-tunnel-providers): provider abstraction + Gateway UI (ngrok/tailscale/zerotier)

Implements OpenSpec change add-tunnel-providers. Extracts a TunnelProvider seam
from the zrok-specific tunnel core (child vs daemon lifecycle), adds ngrok,
tailscale, and zerotier providers behind config.tunnel.provider/mode, multi-
sourced "Accessible at" endpoints, whitelisted server-side enroll, block-event
trusted-network banner, two-QR transport split (pairing QR TLS-only + link QR),
and the "Gateway" settings page + tabbed dialog (UI-only relabel; internals stay
tunnel). Archives + syncs specs (tunnel-provider, qr-device-pairing).

QA/manual tasks deferred to post-merge verification.

* fix: apply CodeRabbit feedback for add-tunnel-providers

- UI: surface config load/save/disconnect errors + catch unhandled rejections
  (GatewayDialog, GatewayPage, GatewayEndpoints); reset stale success on retry
  (GatewaySetupGuide); dismiss block-event banner on wide-subnet trust.
- Security (D3): convert provider execSync string-interpolation to execFileSync
  argv (zrok release/reserve, tailscale + zerotier runner) — closes command
  injection via token/networkId; redact reserved-share tokens in core + zrok logs.
- Correctness: tighten tailscale serveHasHttps to real :443 handlers (no
  false-positive TLS); drop non-existent tailscale:activate enroll step; align
  gateway-endpoints TLS_SCHEME strictness with SECURE_SCHEME.
- Test: zerotier connect-without-networkId edge case.

Deferred (reported): config-api sibling-secret clearing (switch-back UX),
/api/tunnel/endpoints provider-status aggregation, defaultRunner dedup.

* test(tunnel): assert execFileSync argv in releaseShare after injection fix
Adds tests/e2e/reconcile-heal.spec.ts (base change fix-stuck-tool-card-on-
dropped-event, task 5.1): the RECOVERABLE counterpart to superseded-heal
.spec.ts. Drops the tool_execution_end WS frame (server->browser) but leaves
the reconcile route unstubbed, so GET .../tool-result/* hits the real server
(store still holds it) -> 200; asserts the burst flips running->done with the
real echo output and no supersede badge. Verified green against the Docker
harness with PW_CHANNEL=chrome (system Chrome), ~41s heal.

gotoDashboard now arms a one-per-page addLocatorHandler that auto-dismisses the
async first-launch display-preset modal (its backdrop intercepts onboarding
clicks on a wiped/fresh harness; a one-shot check races its render). Makes all
isolated specs robust on a clean container.

Also marks base task 5.1 done and adds the archive-order guard + supersede
cross-reference to the base change proposal/design (must archive with/before
fix-stuck-tool-card-superseded-heal, which MODIFIEs its reconcile requirement).
Telemetry-only proposal to make the in-memory event store's silent shed paths
observable. trimBufferToLimit + evictIfNeeded currently drop events with no
counter, so the deferred Gate B backstop (never-evict latest tool_execution_end,
parked in fix-stuck-tool-card-superseded-heal design.md 'Deferred') cannot be
evidence-gated. Live evidence showed 0 transport drops at both hops but store
trim is unmeasurable by construction; trimBufferToLimit drops oldest tool_*
first, which is exactly the eviction failure mode.

Adds getTrimStats() on the store handle {trimmedEvents:{total,toolExecutionEnd,
bySession},evictedSessions} and surfaces it on /api/health#storeTrim beside the
existing droppedFrames. ADDs a Store-trim instrumentation requirement to
incremental-event-sync. No behavior/protocol/cap change. Validates --strict.
…brella)

Behavioral gate (chat-render-fx.spec.ts, default run):
- Phase 1 reduced-motion contract (tasks 2.7/5.3): live .chat-stream-live
  computed animation-name = chat-stream-glow-pulse/tool-group-sweep, flips to
  none under emulateMedia({reducedMotion:'reduce'}) — real Chrome, not CSS-source.
- task 4.3 auto-scroll-follow bullet: long-transcript overflows + keeps growing
  while the viewport stays pinned to the bottom. (Scroll-up lock, jump-to-turn,
  windowing bounds already gated by chat-transcript-virtualization.spec.ts.)

Advisory perf probe (chat-render-perf.spec.ts, opt-in PW_PERF=1):
- CDP Performance.getMetrics LayoutCount/RecalcStyleCount over a 10s idle window
  on a settled ~120-turn session; idle layouts/s < 30 regression ceiling.
- npm run test:e2e:perf; advisory per design Decision 5 (trace-diff, not budgets).

All 3 green vs the live Docker harness (system Chrome). No app code changed.
…eded-heal

Archive fix-stuck-tool-card-on-dropped-event and
fix-stuck-tool-card-superseded-heal (base-first, dependency order) and
sync their deltas into specs/incremental-event-sync:
  + Stale running-tool reconcile
  + Drop-site delivery instrumentation
  + Superseded terminal heal (modifies Stale running-tool reconcile)

Fix pre-existing main-spec corruption: line-5 header was the delta-only
'## ADDED Requirements', which hid every requirement from openspec
tooling and blocked all syncs; corrected to '## Requirements'.
…nt-event-serialization

Add tests/e2e/oversized-event-liveness.spec.ts automating the change's open
manual task (5.3): drive [[faux:oversized-turn]] (bash ~90KB output -> oversized
event through the real ingest->persist->broadcast JSON.stringify path that used
to OOM-crash the server), assert /api/health stays 200 and a follow-up turn
round-trips in the same session (server alive + responsive).

- qa/fixtures/faux-scenarios.ts: add 'oversized-turn' scenario + OVERSIZED_TURN_MARKER
- sync delta spec (per-event size ceiling + depth-limit collapse) into
  openspec/specs/in-memory-event-buffer/spec.md
- archive change -> openspec/changes/archive/2026-07-12-bound-subagent-event-serialization
- doc rows: tests/e2e/AGENTS.md, qa/AGENTS.md
…nder-cpu-umbrella (tasks 2.8/4.4)

CDP perf probe vs live Docker harness (system Chrome), settled ~120-turn
session: idle layouts/s=1.0, recalcs/s=3.7 over 10s. Vs ~85/s baseline and
under the <5/s target. Consistent with idle-busy <5% (4.4).
…ability specs

Sync (new main specs): chat-event-render-batching, chat-idle-render-cost,
chat-transcript-virtualization (all ADDED capabilities, validated).
Archive → openspec/changes/archive/2026-07-12-reduce-chat-render-cpu-umbrella.
7 manual/verification tasks (1.4, 2.8, 4.3, 4.4, 4.5, 5.1, 5.3) deferred to ship.
Completes the proposal artifacts (proposal.md + specs/content-copy delta
landed in a2c1359). Root-cause: TableWrapper computes CopyButton text
eagerly at render time via a ref read, but React assigns refs at commit;
under React.memo the empty text is frozen so table copy writes "".
Fix direction: CopyButton takes getText: () => string, resolved at click.
…eltTechnology#267)

Capture Layer B of issue BlackBeltTechnology#267: the virtualized chat transcript can leave
image-bearing rows collapsed because ImageAttachments has no onLoad re-measure
and no reserved dimensions, so a row measured before base64 decode stays at its
pre-decode estimate and overlaps its neighbour. Server-side Layer A (per-event
size ceiling) is already fixed on develop by 05239e9; this scopes the client
re-measure hardening only.
…rash-undefined-toolname

Automates the change's manual QA (tasks 5.1 + 5.2) as Playwright e2e:
reducer-poisoned-cache-heal.spec.ts poisons the live IndexedDB
pi-dashboard-replay-cache with an absent-toolName tool_execution_start,
reloads a Dockerized session, and asserts the shell survives (no root
unmount, no toLowerCase pageerror) plus a clean lastSeq:0 full replay
after clearing the cache. Passed PW_CHANNEL=chrome.

Archive the completed change and sync its two ADDED requirements
(event-reducer tolerance, session-replay-persistence fault-isolation)
into the main specs.
…uly-dead -latest aliases

The OAUTH_INCOMPATIBLE table dropped 3 pre-4.x -latest aliases absent from
the live registry catalog, but claude-3-5-haiku-latest IS shipped by the
pi-ai copy the proxy resolves via the tool registry (differs from top-level
node_modules) and is OAuth-unreachable — restore it so /v1/models does not
leak it over Claude Pro/Max OAuth. Verified live via /api/model-proxy/diagnostics.
Spec: fill Purpose + pin the live-catalog -latest denial + maintenance note.
Deterministic tree-sitter code-symbol indexing into the KB Tier-1
nodes/edges graph, retrieved via kb_search + a new find_symbol nav
surface. WASM binding (web-tree-sitter), data-driven language registry
(Core-16 offline + integrity-gated lazy tail), node-as-concept name
model, per-root scoping, pinned perf budget. SCIP precise-xref and
live-LSP enrichment deferred behind the same seam.
…8.2, 9.3)

Grow chat-transcript-virtualization.spec.ts from the 6-test skeleton to 11
passing tests against the docker harness (PW_CHANNEL=chrome), covering the
scroll/streaming/windowing behaviors jsdom cannot reach:

- 9.3: activate the skeleton — timeout calibration (120-turn fixture fires 120
  real bash calls) + expect.poll de-flake; add long-transcript-nav (40-turn)
  faux scenario so turn 0's stat survives MAX_TURN_STATS=50 and the turn-bar
  affordance renders for the off-screen scrollToTurn jump.
- 6.1: resume-within-50px (manual re-arm, no click) + user-scroll-up-mid-replay-wins.
- 4.2: streaming growth keeps the bottom pinned while following.
- 7.2: session-switch scroll persistence restores the anchored row, not bottom.
- 8.2: collapsing an above-viewport tool group does not yank the viewport
  (TanStack measure-driven scroll adjustment + overflowAnchor:none).

Harden tail-wait budgets (waitForTail 240s, test.setTimeout 300s) for real-bash
streaming under load. tsc + biome clean.
New OpenSpec change: keep an active transcript text selection alive across
virtualized-list churn (streaming updates, new cards, auto-scroll).

Design (post doubt-review, incl. deepseek-v4-pro cross-model):
- D1: selectionchange hook; range-intersects-container; proactive anchor-span
  capture into a ref (DOM Range endpoints move synchronously + irreversibly on
  unmount, so retention must be proactive, not reactive).
- D2: suspend auto-scroll bottom-pin while selecting; dep-array + lastScrollHeightRef
  fixes so follow resumes on collapse.
- D3: rangeExtractor unions the tracked span into the mounted range (not bolt-on
  rows); getTotalSize allowed to change; device-aware span ceiling (desktop ~100
  / mobile ~40); past-cap actively removeAllRanges (passive collapse silently
  truncates).
- D4: streaming tail no-worse-than-baseline; node-stable render deferred.
- D5: copy fidelity via copy-event interception deferred; scoped honestly
  (cloneContents for partial rows; slice(0,1000) needs per-renderer cooperation).

Artifacts: proposal, design, specs (new chat-selection-preservation + modified
chat-scroll-lock, chat-transcript-virtualization), tasks. openspec validate passes.
…ange

Grounded diagnosis of 'Gemini 2.5 Pro unusable with subagents': not an
access/auth/tool-schema/model-proxy problem (all refuted live). Root cause
captured via dashboard-spawn D3 repro: Gemini returns a thinking-only turn
(all output tokens reasoning, 0 visible text, no tool call, stopReason=stop,
no error); pi/dashboard idles it silently -> 'no response, nothing reported'.

Artifacts (4/4, validated):
- proposal.md: primary defect = empty-actionable-turn guard; error-surfacing
  complementary; tool-schema sanitizer dropped (parametersJsonSchema accepts
  anyOf/const/format).
- design.md: D3 capture, continue-or-surface guard, upstream adapter-vs-Gemini
  open question.
- specs/empty-actionable-turn-guard, specs/spawned-session-error-surfacing.
- tasks.md: TDD, grounded in bridge.ts stopReason branch + drainFollowupQueue.
Model proxy cannot resolve slash-bearing model ids: split("/", 2)
truncates at the first slash, so openrouter/anthropic/claude-3.5-sonnet
404s despite being advertised by /v1/models. Also adds a preferred-model
mechanism (none exists beyond a single defaultModel fallback).

Change captures:
- fix: first-slash parse via shared parseModelId helper + round-trip invariant
- deterministic source precedence (dedup by fqid; built-in > custom > models.json)
- new modelProxy.preferredModels (ordered availability fallback) + modelAliases
- Settings UI reusing ModelSelector for model picking (+ mockup)

Artifacts only (proposal/design/spec-delta/tasks/mockup); no impl.
- Fold the automatable slice of the task 10.1 perf gate into the windowing
  test: a 120-turn session measures 179 transcript DOM nodes vs the 46,918
  Step-A baseline (< 3000 ceiling), proving nodes track the viewport working
  set, not session length (design Decision 7). GC/heap/listener/idle-busy
  remain a manual DevTools trace.
- De-flake "streaming tail stays mounted": replace the single-snapshot
  live-count read with a retrying toBeAttached — the .chat-stream-live class
  briefly drops at 120-turn boundaries; the invariant is that scrolling up
  never windows the live tail away. Verified 3/3 under --repeat-each.

Full suite 11 passed via PW_CHANNEL=chrome. tsc + biome clean.
…pecs

Move the completed change to archive/2026-07-12-virtualize-chat-transcript-tanstack
and sync the chat-transcript-virtualization capability spec to the Step-B
(windowing) requirements: mounted-node/listener bound, scroll-lock preserved,
off-screen scrollToTurn top-align, streaming-tail pinning, per-session restore.

Remaining perf-trace (GC/heap/listeners/idle) + manual smoke tasks are deferred
(to be verified later); all browser-testable behavior is covered by the e2e suite.
…ckBeltTechnology#273)

* docs(openspec): fold doubt-review findings into scroll-to-top design

Cross-model (deepseek-v4-pro) adversarial review + source verification:
- scrollToIndex(0) is bounded (maxAttempts=10, not infinite); re-issue on
  async img.onload so a late image-load remeasure can't bump off index 0
- guard the scroll-to-top re-arm race with an ascendingRef latch so
  handleScroll can't flip stickToBottomRef back on mid-flight
- image reserve is per-renderer-kind (300px user / 512px toolResult),
  not one global constant
- Decision 1 constants gated on the required Playwright e2e convergence
  gate; jsdom demoted to logic-only (shim can't reproduce the timing race)
- mechanism note: stale-offset story inferred, not version-pinned

* fix(chat): content-aware row estimate + scroll-to-top affordance

Scrolling up a virtualized transcript never converged on index 0: the
largest rows (pasted image, 24k-char tool result) sit near the top and
the static per-role estimate under-shot them 10-50x, so on mount
getTotalSize() jumped and the top receded.

- chat-virtual-rows.ts: estimateVirtualRowSize is now content-aware
  (base + ceil(textChars/80)*20, clamped 8000px, + per-kind image reserve
  300 user / 512 toolResult); computeRowTextChars aggregates rendered text
  once per displayRows rebuild so estimateSize stays O(1). Shrinking the
  estimate delta shrinks TanStack's built-in above-viewport correction
  below perception.
- ChatView.tsx: precompute rowTextChars in the displayRows useMemo; add a
  scroll-to-top button (ascendingRef latch). scrollToIndex(0) is bounded
  (maxAttempts=10), so onChange re-issues it on measurement growth (covers
  async image-load remeasure); handleScroll ascending branch prevents the
  bottom-pin re-arm race; cancelDescent clears the latch on user input.
  No manual scrollTop+=delta (would double-move); overflowAnchor:none kept.
- Tests: unit (monotonic estimate, per-kind image reserve, aggregate
  textChars) + scroll-to-top logic guards (button visibility, re-arm race).
  Browser-timing convergence is Playwright-gated (jsdom shim can't
  reproduce it).

See change: fix-chat-scroll-to-top-estimate-drift

* test(e2e): scroll-to-top convergence gate + top-heavy faux fixture

The required browser-timing gate jsdom cannot provide (doubt-review #4/#5):

- qa/fixtures/faux-scenarios.ts: buildScrollTopHeavy() + scroll-top-heavy
  scenario — biggest rows (16k thinking, 9k text, 24k bash toolResult,
  inline image) near the TOP, then ~40 small turns. Exported
  SCROLL_TOP_HEAVY_TAIL.
- tests/e2e/scroll-to-top.spec.ts: 2 tests via [[faux:scroll-top-heavy]] —
  (1) scroll-to-top lands scrollTop<=2 and STAYS after the near-top image
  loads async (the bounded-scrollToIndex re-issue path); (2) incremental
  scroll-up converges on scrollTop<=2 + [data-index=0] visible (top does
  not recede). test.slow() (180s) for the ~40 real bash round-trips.
- helpers: scrollToTop testid.

Verified 2/2 green against the Docker harness. Test 2 fails if the top
recedes (the pre-fix bug); test 1 fails without the new button — the gate
is not vacuous.

See change: fix-chat-scroll-to-top-estimate-drift

* chore(openspec): archive fix-chat-scroll-to-top-estimate-drift

Sync delta specs into openspec/specs/chat-transcript-virtualization.
QA/manual tasks were validated via the Playwright e2e gate + existing
virtualization suite.

* test: add clamp-boundary + empty-payload edge cases (CodeRabbit)

Covers exact text-reserve clamp boundary + saturation above it, and
0-length message/group/burst payloads for the O(1) sizing contract.
…G via electron-builder (BlackBeltTechnology#277)

Rewire build_native() to mirror CI (_electron-build.yml): darwin uses
electron-forge package -> electron-builder --mac dmg --prepackaged (with
CSC_IDENTITY_AUTO_DISCOVERY=false); linux uses forge make -> electron-builder
--linux AppImage. Fixes the hard failure where npm run electron:build could not
produce a macOS DMG. Removes obsolete macos-alias plumbing (ensure-macos-alias
postinstall, maker-dmg devDep, Doctor row + test) since electron-builder's DMG
target uses hdiutil, not macos-alias.

Adds tests/e2e-electron/dmg-build-launch.electron.spec.ts (Playwright _electron)
that mounts the built DMG and asserts the app launches + dashboard opens.

Implements OpenSpec change fix-local-electron-dmg-build. Archives + syncs specs.
QA/manual tasks (arm64 --mac-both) deferred to post-merge CI verification.
…c decode (BlackBeltTechnology#267) (BlackBeltTechnology#275)

* fix(fix-virtualized-image-row-measurement): re-measure image rows on decode

Image-bearing user rows in the virtualized transcript could collapse and
overlap their neighbour (issue BlackBeltTechnology#267): a base64 data-URL decodes async, so a
row is first measured near-zero and, when the decode-driven ResizeObserver
re-measure is missed (ChatView reuse across session switch), stays cached at
the collapsed height.

- ImageAttachments: onImageLoad re-measures the owning virtual row via
  virtualizer.measureElement, coalesced to one measure per row per frame.
- Reserve a bounded loading box on the <img> so the pre-decode measurement
  is not near-zero (dropped once decoded).
- estimateVirtualRowSize: taller pre-mount estimate for image-bearing user
  rows to bound first-paint drift (measurement stays authoritative).

Archives + syncs specs. QA/manual tasks (5.1/5.2) deferred to post-merge.

* merge develop: drop superseded estimate edit, keep onLoad re-measure

develop PR BlackBeltTechnology#273 rewrote estimateVirtualRowSize into a content-aware form
(estimateVirtualRowSize(item, textChars)) that already reserves
IMAGE_RESERVE_USER=300 for image rows. Take develop's chat-virtual-rows.ts
wholesale — our interim task-3.1 estimate bump is superseded and its textual
auto-merge broke the new baseRowSize(role) scope (TS2304 msg).

The load-bearing fix (ImageAttachments onLoad -> coalesced row re-measure +
reserved loading box in ChatView.tsx) is independent of the estimate rewrite
and preserved. tsc clean; full client suite 3195 green.

* fix: apply CodeRabbit feedback for fix-virtualized-image-row-measurement

- Add onError-path test: a failed image decode must not schedule a re-measure.
- Archived design.md: convert the ASCII decode-sequence to a Mermaid
  sequenceDiagram (repo Diagram Style rule).
…time (BlackBeltTechnology#276)

CopyButton now takes a getText callback resolved at click time instead of an
eager text string bound at render, so ref-derived payloads (tables, code blocks,
message body) copy real content even under React.memo single-render. Migrates
all call sites. Adds click-level unit tests + a Playwright e2e (copy-surfaces
faux scenario, real clipboard) automating the former manual QA.

Implements OpenSpec change fix-table-copy-empty-clipboard. Archives + syncs specs.
Repairs pre-existing structural corruption in openspec/specs/content-copy/spec.md
(leaked delta header + missing Purpose section) to allow the sync.
robertcsakany and others added 2 commits July 17, 2026 00:38
…s from payload (no file read) (BlackBeltTechnology#346)

* plan(bundle-python-runtime): doubt-reviewed proposal+design+specs+tasks

Planning artifacts for bundling a pip-capable Python env (uv + python-build-standalone,
Tier 2) into the Electron bundle, exposed on PATH for agent bash/tool calls all platforms.

Doubt-review (single + cross-model GLM) corrected 13 findings, notably:
- D5 rewritten: sync-inject + bare-interpreter fallback (async materialize can't be
  awaited in the synchronous spawn-env seam)
- UV_* scoped to materialize subprocess; SSL_CERT_FILE -> interpreter CA (pip certs)
- Windows rebuild-vs-live-install via versioned overlay dirs
- offline base rebuild = online/warm-cache only (Tier 2 non-goal)

scenario-design gate resolved 2 spec gaps (offline pip = pip's own exit/stderr passthrough;
small hash-pinned starter baseline). 24 automated scenarios folded to tasks (test-plan.md
manifest), 0 manual-only. #9 uv file:// mirror layout gated behind a blocking spike.

* docs(security): plan add-universal-network-guard (doubt-review + scenario-design)

plan-proposal on develop. Added design.md + test-plan.md; revised proposal/
specs/tasks after a 2-cycle doubt-driven-review (single-model + cross-model
@propose-review-1 GLM, @propose-review-2 DeepSeek).

Design pivoted: the first draft's "deny-all + enumerate a public allowlist" was
shown unimplementable (hashed static assets rooted at /, SPA history-fallback via
setNotFoundHandler) and would brick the app shell with auth off over a tunnel.
Revised to a namespace-scoped guard: jurisdiction = /api,/v1,/editor,/live,
deny-by-default within, everything else (static/SPA/auth/manifest) untouched.

Reviewer-found + reconciled:
 - /v1 proxy brick: proxy gate must set request.isAuthenticated; guard registered
   last (after bearer/oauth/proxy hooks).
 - /auth/status + SPA shell + deep-link refresh must stay public (now out of
   jurisdiction by construction).
 - model-proxy SECOND port (server.ts:1875) is a separate Fastify instance with
   only the proxy gate -> loopback-bind invariant, test-asserted.
 - proposal/design contradiction (ws-ticket mint) + phantom service worker fixed.

scenario-design: test-plan.md manifest, 18 automated (17 L1 + 1 L3) + 1
manual-only, folded into tasks.md as vanilla checkboxes with exemplar+Triple+
manifest ref. openspec validate --strict passes; all 4 artifacts done.

* plan(openspec-artifact-dialog-desktop): proposal+design+specs+tasks+test-plan

Desktop-viewport modal for P/D/S/T artifact badges; mobile keeps full-page route. Planned via plan-proposal: 2 doubt-review cycles (single+cross-model), scenario-design manifest (20 automated + 1 manual), folded to tasks.

* plan(cloud-sync): add-cloud-sync-connector planning artifacts

Bidirectional local↔cloud file sync connector (Drive/Dropbox/OneDrive).
Baseline-ledger engine, held-conflict resolution, provider adapter seam.

- proposal.md: scope, safety posture (lossless+faithful), pinned decisions
- design.md: impossibility triangle, 8 decisions, 3 engine invariants,
  pressure-tested Provider interface (7 verbs)
- specs/cloud-file-sync/spec.md: 42-scenario completeness matrix (checkable
  total-coverage backbone), N=3 hot-edit defer, K=10 tombstone window
- test-plan.md: manifest, 45 automated (L1/L2) + 2 manual-only, functional-only
- tasks.md: 68 tasks, folded automated scenarios (parser-safe vanilla checkboxes)

Doubt-reviewed 3 cycles (single + cross-model glm-5.2): fixed 4 data-loss paths
(conflict→push re-overwrite, pull pre-write overwrite, native-doc push,
resolve pull-inversion).

* docs(openspec): shelve close-initialize-handoff-via-bus (never implemented)

Doubt-review (single-model + GLM-5.2 cross-model) found the design incoherent
with the shipped server, defeating its own no-click goal via the TOFU trust
resolution, and resting on a wrong value premise:

- No bus primitive awaits worktree_init_done (keyed by requestId+cwd, not a
  session status); plain spawn({cwd}) doesn't fire the hook and re-launches
  project-init. until()/await()/read cannot observe completion.
- TOFU trust gate: auto-confirming a just-written hook bypasses the human gate
  and poisons worktree-init-trust.json, so the next manual click skips TOFU.
- Premise wrong: project-init's worktreeInit hook is ~npm ci; kb-index is the
  kb-extension's separate hook; openspec-init already runs inline at Step 6.
- connect() has no timeout arg; connect-failed is never thrown -> bounded
  degradation is unimplementable as designed.

Archived pre-implementation with NOTES.md capturing findings + revival
prerequisites so it is not re-proposed.

* feat(session-diff): opt-in out-of-cwd session diffs (payload-only, no file read)

Render diffs for files a session writes OUTSIDE its workspace from the captured
Write/Edit event payload — the server never reads the out-of-cwd file (the
doubt-review's core property).

Server:
- session-diff.ts: `resolvePathKey` carries out-of-cwd Write/Edit entries keyed
  by absolute path; `buildSessionDiff` splits in-cwd (enriched) vs out-of-cwd
  (payload-only, previewable:false) BEFORE enrichment so an out-of-cwd path can
  never reach the untracked `readFileSync(resolve(cwd,absPath))` branch. Threads
  `toolCallId` + `truncated` onto FileChangeEvent.
- session-file-reader.ts: `findSessionToolCallPayload(file, toolCallId)` returns
  the FULL untruncated Write/Edit payload from the on-disk JSONL (session-scoped
  id only, no path input/traversal).
- routes: `GET /api/session-change/:sessionId/:toolCallId` → { content?, edits? }
  resolved via sessionManager.sessionFile; miss → 404, reads nothing else.
- preferences-store: backfill + merge `showOutOfCwdSessionDiffs` (default off).

Client:
- DisplayPrefs.showOutOfCwdSessionDiffs (default off, all presets) + ChatViewMenu
  toggle. ChatView suppresses out-of-cwd rows unless on.
- DiffPanel: hide the File toggle when previewable:false; lazy-fetch full payload
  on truncation; truncation banner on fetch failure (never blank).
- diff-tree: out-of-cwd entries group under "outside workspace" (no blank-root).
- normalize-path: `isOutOfCwd`.

Tests: L1 E1-E8 + X3 + truncation threading (session-diff/reader/routes/prefs);
unit F4 (tree) + isOutOfCwd + F5/F3/X2 (DiffPanel); L3 e2e F1/F2/F5 + API-level
F3 (out-of-cwd-session-diffs.spec.ts, harness-verified). X1 superseded by the
payload-only design (never reads the file → deletion is moot).

OpenSpec: opt-in-out-of-cwd-session-diffs

* chore(opt-in-out-of-cwd-session-diffs): archive + sync specs

Archives the change to openspec/changes/archive/ and syncs the
change-summary-table + session-diff-extraction delta specs into
openspec/specs/. Manual-only QA task deferred to post-merge verification.

* fix: apply CodeRabbit feedback for opt-in-out-of-cwd-session-diffs

- DiffPanel: accept the lazy-fetch payload only when it carries a string
  `content` or an `edits` array ({success:true,data:{}} → truncation banner,
  not a silent blank); reset viewMode to Diff when a refresh flips an entry to
  out-of-cwd (previewable:false) in File mode so the /api/session-file fetch
  (403 for out-of-cwd) can never fire.
- Tests: F3 now asserts the fetched full content REPLACES the truncated text in
  the render (RichDiff mock exposes newText) + a previewable→false rerender test;
  isOutOfCwd(undefined-cwd) case; E3 also asserts no existsSync/statSync probe of
  the out-of-cwd path; nested-toolCallId test uses a distinct decoy top-level id.

Skipped (with reason): archive-dir relocation (the change is correctly archived
by the ship workflow); oversized AGENTS.md row splits (pre-existing giant rows —
out of scope); e2e session-scan/marker refinements (reliable as-is on a fresh
container, endpoint content assertion already proves full fidelity).
# Conflicts:
#	CHANGELOG.md
#	docs/AGENTS.md
#	package-lock.json
#	packages/client/src/components/ChatView.tsx
#	packages/client/src/components/ChatViewMenu.tsx
#	packages/client/src/components/ChatViewMenu.tsx.AGENTS.md
#	packages/client/src/components/CommandInput.tsx
#	packages/client/src/components/SessionCard.tsx
#	packages/client/src/components/SettingsPanel.tsx
#	packages/client/src/components/ToolCallStep.tsx
#	packages/client/src/components/__tests__/ChatViewMenu.flip.test.tsx
#	packages/client/src/components/interactive-renderers/SelectRenderer.tsx
#	packages/client/src/hooks/AGENTS.md
#	packages/client/src/hooks/usePopoverFlip.ts
#	packages/client/src/lib/AGENTS.md
#	packages/extension/src/__tests__/role-manager.test.ts
#	packages/extension/src/bridge.ts
#	packages/extension/src/role-manager.ts
#	packages/roles-plugin/src/RolesSettingsSection.tsx
#	packages/roles-plugin/src/__tests__/RolesSettingsSection.test.tsx
#	packages/server/package.json
#	packages/server/src/AGENTS.md
#	packages/server/src/event-wiring.ts
#	packages/server/src/routes/system-routes.ts
#	packages/server/src/server.ts
@joeshull

Copy link
Copy Markdown
Author

Superseded by the latest automated upstream sync on chore/upstream-sync-tooling. Only the newest sync PR is kept open for review.

1 similar comment
@joeshull

Copy link
Copy Markdown
Author

Superseded by the latest automated upstream sync on chore/upstream-sync-tooling. Only the newest sync PR is kept open for review.

@joeshull joeshull closed this Jul 16, 2026
Joe added 7 commits July 16, 2026 16:46
Add scripts/upstream-sync.sh (status/merge/verify/pr), docs policy,
weekly upstream-sync workflow, and ci-zge focused gates so we can merge
BlackBelt develop without clobbering deploy/push/OMP surfaces. Default
install ref is main; README links the sync runbook.
Use stable branch sync/upstream-<ref>, force-with-lease each run, upsert
the same PR, and close older labeled/sync-head PRs as superseded so a
late review only has the latest merge to look at.
Gate 1 invoked vitest with real HOME, which upstream globalSetup
rejects. Match root npm test isolation and pass package vitest.config.
@joeshull joeshull reopened this Jul 16, 2026
@joeshull
joeshull force-pushed the sync/upstream-develop branch from 44f0cc1 to 0ea9cee Compare July 16, 2026 22:54
@joeshull joeshull changed the title chore(sync): CONFLICTS upstream develop @ f1800272 chore(sync): merge upstream develop @ e75445fc Jul 16, 2026
@joeshull
joeshull marked this pull request as ready for review July 16, 2026 22:54
Joe added 5 commits July 16, 2026 16:56
Quote PR body heredoc to avoid backtick command substitution; refuse
force-with-lease on main/develop/target; recompute SYNC_BRANCH after
--ref; simplify workflow conflict-help echoes.
Precedence: --branch > env SYNC_BRANCH > sync/upstream-<ref>.
Capture SYNC_BRANCH_FROM_ENV before applying the default so an explicit
SYNC_BRANCH equal to the default name still wins over --ref.
Copy docs/upstream-sync-conflicts-YYYYMMDD.md to /tmp before abort and
restore onto the stable sync branch so the draft PR includes inventory.
Prevent SYNC_BRANCH=foo from force-publishing a non-sync head that
stale-PR closing would never supersede.
@joeshull joeshull changed the title chore(sync): merge upstream develop @ e75445fc chore(sync): upstream develop @ e75445fc Jul 16, 2026
Gateway already emits structured errors for unknown plugin actions;
handle them in useMessageHandler so they are not silently dropped.
@joeshull

Copy link
Copy Markdown
Author

Addressed review: client now toasts plugin_action_error (unknown pluginId action) instead of silent drop.

@joeshull
joeshull merged commit b57dfdd into main Jul 17, 2026
1 check passed
@joeshull
joeshull deleted the sync/upstream-develop branch July 17, 2026 01:15
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

upstream-sync Automated or manual upstream merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants