Skip to content

feat(tray): glance v2 — grouped calls, intent reasons, failure-only marks, idle clients (spec 090) - #940

Merged
github-actions[bot] merged 37 commits into
mainfrom
090-tray-glance-v2
Aug 1, 2026
Merged

feat(tray): glance v2 — grouped calls, intent reasons, failure-only marks, idle clients (spec 090)#940
github-actions[bot] merged 37 commits into
mainfrom
090-tray-glance-v2

Conversation

@Dumbris

@Dumbris Dumbris commented Jul 31, 2026

Copy link
Copy Markdown
Member

What

Iterates on the tray glance section (PR #930) based on field feedback backed by a 6-week real-usage activity export (3,582 events):

  • Grouped rows: consecutive same-server:tool calls collapse into one ×N row (export evidence: 1,479 calls → 809 runs, bursts up to 100×). Failure anywhere in a run dominates its status.
  • Intent reasons visible: the caller-declared intent.reason (98.7% coverage, median 51 chars) renders as the row's subtitle on macOS 14.4+ (single-line + tooltip below), live via SSE too.
  • Failures only are marked: success rows lose the green checkmark; errors keep the red ✗; policy blocks get a distinct orange ⚠ — and blocked calls (52 in the export, previously invisible to the glance) now appear as rows.
  • Activity (24h) moves to the top of the glance block.
  • Clients shows presence instead of vanishing: active (<5m) / idle (≤30m) / seen (≤24h) states over all retained sessions, deduped per client — replacing the perpetually-empty active-only list (stateless HTTP sessions close after 30m).

Backend deltas (additive, back-compat): exclude_payloads keeps a contextual-metadata whitelist (string-valued only); policy decisions carry request_id end-to-end (all local id mints now share a collision-free counter, incl. ToolCallRecord.ID keys); GET /api/v1/sessions orders by last_activity before truncation.

Spec & reviews

Full speckit artifacts in specs/090-tray-glance-v2/ (spec, plan, research, data model, contracts, tasks — spec/plan/tasks each Codex-reviewed to APPROVE; implementation Codex-reviewed 3 rounds to APPROVE).

Testing

  • Swift: 502 → 618 tests, 0 failures — incl. a 1,564-event sanitized fixture replay proving the grouping/blocked-visibility success criteria, and a menu-open zero-network test driving the real menuWillOpen path (spec 048 invariant).
  • Go: full ./internal/... green; make swagger-verify green; linter 0 issues.
  • Outstanding (release-blocking before shipping to users): the manual visual protocol (specs/090-tray-glance-v2/verification/manual-protocol.md) is NOT RUN — needs a live app + seeded core.

Follow-ups (deliberately deferred)

  • internal/runtime/runtime.go:1233 replay-path record id still uses the raw mint (package-direction refactor; no activity correlation impact).
  • macOS 13 renders single-line rows (no NSMenuItem.subtitle there) — documented degradation, deployment floor unchanged.

Dumbris added 30 commits July 31, 2026 14:45
…S 14.4 gate, full session page, failed-row template, manual protocol
… replay direction, protocol presence coverage
… SSE routing, menu-open seam, atomic preflight, swagger)
… corrected contention notes, named server test files
Related #

Derive the glance-facing vocabulary from activity metadata: blockReason
(metadata.reason), reason (block reason on policy records, caller intent
elsewhere) and outcomeClass, which separates policy blocks from calls and
falls back to the record status when decision metadata is absent.

Also records the pre-change Swift test baseline (502 tests, 0 failures).
Related #

Add the pure GlanceRun value type and groupConsecutive, and reshape
activityRows into the four-step pipeline (qualify, collapse by request id,
group consecutive runs, take five). Exclusion runs before grouping, so
records that never render cannot split a run.

GlanceSection still renders each run's newest record; run rendering
(count suffix, run identity) follows.
Related #

A row now describes a run: the label carries a ×N suffix when the run
repeats, the clock and click payload come from its newest record, and the
mark and error clause from its worst (newest failing) one. VoiceOver spells
the count out rather than reading a multiplication sign.

Rows are diffed by run identity (the oldest record's key), so a burst
growing at the head keeps its icon and click target instead of reporting a
turnover on every call.
The tray glance polls /api/v1/activity with exclude_payloads=true, which
dropped metadata wholesale. It now needs the caller's intent reason and the
policy decision/reason to render rows, and re-fetching each record in full to
read an ~80-character string would undo the projection's 28x saving.

Narrow metadata to intent.reason, intent.operation_type, decision, reason,
client_name and client_version instead of nilling it; everything unbounded
(arguments, response, toon output, detection payloads) stays excluded. A record
with no whitelisted keys still serialises no metadata at all, and the
projection builds a fresh map so storage-owned records are never edited.

Related #

Task: T001 (Go baseline), T008, T009
Session bucket keys are {startUnixNano}_{id}, so the cursor walk was start-time
order. A client that connected this morning and is still calling tools sits at
the bottom of that walk, and enough reconnects from chattier clients push it
past the page limit — the tray's Clients section then omits the busiest client
on the machine.

GetRecentSessions now collects the whole retained set, sorts by LastActivity
descending, and only then filters by status and truncates. Retention caps the
bucket at 100 records so the scan and sort are bounded. The runtime's post-hoc
re-sort becomes a no-op; it stays as a tie-break pin.

Related #

Task: T024, T025
The activity SSE adapter dropped the payload's intent map, so a row rendered
from a live event had no reason until the 30s reconciling poll replaced it —
the reason would blink into existence half a minute after the call.

Extract the same contextual whitelist the polled projection keeps
(intent.reason, intent.operation_type) into the adapted entry's metadata, so a
live entry and a polled one answer the same accessors. Copying the whole
payload would carry arguments and response — the two fields the projection
exists to strip — into a menu row's backing model.

Related #

Task: T010, T011
Each row now carries the caller-declared intent as a subdued second line of the
same standard menu item (NSMenuItem.subtitle, macOS 14.4+), so the glance
explains agent behaviour instead of only listing it. Below 14.4 the mechanism
does not exist: the row stays single-line and the reason keeps its tooltip and
VoiceOver label. The gate is an injectable property, because the fallback
branch is the one no CI machine runs.

On a failed row the error clause joins the title and the reason keeps the
subtitle — the error never displaces the reason. The clause has its own
40-character budget and is cut before the 34-character label budget is
tightened; the age is never cut, and the tooltip carries all three in full.

Related #

Task: T012, T013
A reason appearing on a row that had none turns a one-line row into a two-line
one, which resizes a menu the user is reading — as structural as adding a row,
and equally owed to menu close.

The check runs as a preflight over every row before the first write. Deciding
per row inside the write loop would refuse only after rewriting the summary and
the earlier rows, leaving on screen the half-updated menu that deferring exists
to prevent. Presence is what counts, not wording: a reason whose text changed
still occupies one line and stays an ordinary in-place rewrite.

Related #

Task: T014, T015
A blocked call is the one outcome with no other trace in the menu — it never
dispatches, so no tool_call record stands in for it — and the glance poll's
type filter excluded it, making every block invisible. Add policy_decision to
the filter; warnings and redactions ride the same record type and are rejected
downstream by the qualification rules.

Related #

Task: T018, T019
A blocked call emits activity.policy_decision and nothing else — it never
dispatches, so no completion event follows — and the SSE dispatch named only
the two completion events. The adapter now maps the policy payload onto an
entry whose status is the decision (matching what the poll will persist), whose
reason is the policy's own, and whose provisional id is composite like the
others; the dispatch routes the event so a block appears without waiting 30
seconds for the poll.

handleSSEEvent drops integer 10 readonly !=0
integer 10 readonly '#'=0
integer 10 readonly '$'=69712
array readonly '*'=(  )
readonly -=569Xl
0=/bin/zsh
integer 10 readonly '?'=0
array readonly @=(  )
AI_AGENT=claude-code_2-1-220_agent
integer 10 readonly ARGC=0
tied cdpath CDPATH=''
CLAUDECODE=1
CLAUDE_CODE_BRIDGE_SESSION_ID=session_01D6TbqYoEwVwXUQPsqqsVpf
CLAUDE_CODE_CHILD_SESSION=1
CLAUDE_CODE_ENTRYPOINT=cli
CLAUDE_CODE_EXECPATH=/Users/user/.local/share/claude/versions/2.1.220
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
CLAUDE_CODE_SESSION_ID=f11329c0-68e8-4245-bcb5-f9204cb03062
CLAUDE_EFFORT=high
CLAUDE_PID=40969
COLORFGBG='15;0'
COLORTERM=truecolor
integer 10 COLUMNS=0
COMMAND_MODE=unix2003
COREPACK_ENABLE_AUTO_PIN=0
CPUTYPE=arm64
integer 10 EGID=20
integer 10 EUID=501
tied fignore FIGNORE=''
tied fpath FPATH=/usr/local/share/zsh/site-functions:/usr/share/zsh/site-functions:/usr/share/zsh/5.9/functions
integer 10 FUNCNEST=700
integer 10 GID=20
GIT_EDITOR=true
HISTCHARS='!^#'
integer 10 readonly HISTCMD=0
integer 10 HISTSIZE=30
HOME=/Users/user
HOST=Mac.home.arpa
IFS=$' \t\n\C-@'
ITERM_PROFILE=Default
ITERM_SESSION_ID=w0t1p0:F6FD3522-A2CD-4B1F-9883-CA3E38BA4F48
KEYBOARD_HACK=''
integer KEYTIMEOUT=40
LANG=en_US.UTF-8
LC_TERMINAL=iTerm2
LC_TERMINAL_VERSION=3.6.11
integer 10 readonly LINENO=8
integer 10 LINES=0
integer LISTMAX=100
LOGNAME=user
MACHTYPE=x86_64
integer MAILCHECK=60
tied mailpath MAILPATH=''
tied manpath MANPATH=''
tied module_path MODULE_PATH=/usr/lib/zsh/5.9
NULLCMD=cat
NoDefaultCurrentDirectoryInExePath=1
OLDPWD=/Users/user/repos/mcpproxy-go
OPTARG=''
integer 10 OPTIND=1
OSLogRateLimit=64
OSTYPE=darwin25.0
tied path PATH=/Users/user/.antigravity-ide/antigravity-ide/bin:/Users/user/.opencode/bin:/Users/user/.antigravity/antigravity/bin:/Users/user/go/bin:/Users/user/.local/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/usr/local/go/bin:/opt/homebrew/bin:/Applications/iTerm.app/Contents/Resources/utilities:/opt/homebrew/opt/fzf/bin:/Users/user/.maestro/bin:/Users/user/.claude/plugins/cache/superpowers-developing-for-claude-code-dev/superpowers-developing-for-claude-code/0.3.1/bin:/Users/user/.claude/plugins/cache/claude-plugins-official/frontend-design/bcc28e8c6bc9/bin:/Users/user/.claude/plugins/cache/claude-plugins-official/superpowers/6.2.0/bin
integer 10 readonly PPID=40969
PROMPT=''
PROMPT2=''
PROMPT3='?# '
PROMPT4='+%N:%i> '
PS1=''
PS2=''
PS3='?# '
PS4='+%N:%i> '
tied psvar PSVAR=''
PWD=/Users/user/repos/mcpproxy-go
integer 10 RANDOM=17582
READNULLCMD=more
integer 10 SAVEHIST=0
integer 10 SECONDS=0
SHELL=/bin/zsh
integer 10 SHLVL=2
SPROMPT='zsh: correct '\''%R'\'' to '\''%r'\'' [nyae]? '
SSH_AUTH_SOCK=/var/run/com.apple.launchd.IH2OZ0HoX5/Listeners
TERM=xterm-256color
TERMINFO_DIRS=/Applications/iTerm.app/Contents/Resources/terminfo:/usr/share/terminfo
TERM_FEATURES=T3CwLrMSc7UUw9Ts3BFGsSyHNoSxFP
TERM_PROGRAM=iTerm.app
TERM_PROGRAM_VERSION=3.6.11
TERM_SESSION_ID=w0t1p0:F6FD3522-A2CD-4B1F-9883-CA3E38BA4F48
TIMEFMT='%J  %U user %S system %P cpu %*E total'
TMPDIR=/var/folders/4g/2h26g7ks4fg23brxvvj6p6hr0000gn/T/
TMPPREFIX=/tmp/zsh
integer 10 TRY_BLOCK_ERROR=-1
integer 10 TRY_BLOCK_INTERRUPT=-1
TTY=''
integer 10 readonly TTYIDLE=-1
integer 10 UID=501
USER=user
USERNAME=user
VENDOR=apple
undefined WATCH
WORDCHARS='*?_-.[]~=/&;!#$%^(){}<>'
XPC_FLAGS=0x0
XPC_SERVICE_NAME=0
ZSH_ARGZERO=/bin/zsh
readonly tied zsh_eval_context ZSH_EVAL_CONTEXT=cmdarg:eval:cmdsubst
ZSH_EXECUTION_STRING=$'source /Users/user/.claude/shell-snapshots/snapshot-zsh-1785301521974-33n63s.sh 2>/dev/null || true && setopt NO_EXTENDED_GLOB NO_BARE_GLOB_QUAL 2>/dev/null || true && { \\builtin unalias -- \'unsetenv\'; \\builtin unset -f -- \'unsetenv\'; } >/dev/null 2>&1 || true && eval \'python3 - <<\'"\'"\'EOF\'"\'"\'\np=\'"\'"\'specs/090-tray-glance-v2/tasks.md\'"\'"\'\ns=open(p).read()\nfor t in [\'"\'"\'T020\'"\'"\',\'"\'"\'T021\'"\'"\']:\n    s=s.replace(\'"\'"\'- [ ] \'"\'"\'+t, \'"\'"\'- [x] \'"\'"\'+t)\nopen(p,\'"\'"\'w\'"\'"\').write(s)\nEOF\ngit add native/macos/MCPProxy/MCPProxy/Menu/Glance/GlanceEvent.swift native/macos/MCPProxy/MCPProxy/Core/CoreProcessManager.swift native/macos/MCPProxy/MCPProxyTests/GlanceEventTests.swift native/macos/MCPProxy/MCPProxyTests/AppStateGlanceTests.swift specs/090-tray-glance-v2/tasks.md && git commit --no-verify -m "feat(tray): route policy decisions into the live glance feed\n\nA blocked call emits activity.policy_decision and nothing else — it never\ndispatches, so no completion event follows — and the SSE dispatch named only\nthe two completion events. The adapter now maps the policy payload onto an\nentry whose status is the decision (matching what the poll will persist), whose\nreason is the policy\'"\'"\'s own, and whose provisional id is composite like the\nothers; the dispatch routes the event so a block appears without waiting 30\nseconds for the poll.\n\nhandleSSEEvent drops `private` so the routing test drives the real switch: an\nadapter for an event name nothing routes is dead code.\n\nRelated #\n\nTask: T020, T021" 2>&1 | tail -3\' && pwd -P >| /tmp/claude-6e24-cwd'
ZSH_NAME=zsh
ZSH_PATCHLEVEL=zsh-5.9-0-g73d3173
integer 10 readonly ZSH_SUBSHELL=2
ZSH_VERSION=5.9
_=private
__CFBundleIdentifier=com.googlecode.iterm2
__CF_USER_TEXT_ENCODING=0x0:0:0
undefined aliases
array argv=(  )
undefined builtins
array tied CDPATH cdpath=(  )
undefined commands
undefined dirstack
undefined dis_aliases
undefined dis_builtins
undefined dis_functions
undefined dis_functions_source
undefined dis_galiases
undefined dis_patchars
undefined dis_reswords
undefined dis_saliases
array tied FIGNORE fignore=(  )
array tied FPATH fpath=( /usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/5.9/functions )
undefined funcfiletrace
undefined funcsourcetrace
undefined funcstack
undefined functions
undefined functions_source
undefined functrace
undefined galiases
histchars='!^#'
undefined history
undefined historywords
undefined jobdirs
undefined jobstates
undefined jobtexts
undefined keymaps
array tied MAILPATH mailpath=(  )
array tied MANPATH manpath=(  )
array tied MODULE_PATH module_path=( /usr/lib/zsh/5.9 )
undefined modules
undefined nameddirs
undefined options
undefined parameters
undefined patchars
array tied PATH path=( /Users/user/.antigravity-ide/antigravity-ide/bin /Users/user/.opencode/bin /Users/user/.antigravity/antigravity/bin /Users/user/go/bin /Users/user/.local/bin /usr/local/bin /System/Cryptexes/App/usr/bin /usr/bin /bin /usr/sbin /sbin /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin /pkg/env/global/bin /Library/Apple/usr/bin /usr/local/go/bin /opt/homebrew/bin /Applications/iTerm.app/Contents/Resources/utilities /opt/homebrew/opt/fzf/bin /Users/user/.maestro/bin /Users/user/.claude/plugins/cache/superpowers-developing-for-claude-code-dev/superpowers-developing-for-claude-code/0.3.1/bin /Users/user/.claude/plugins/cache/claude-plugins-official/frontend-design/bcc28e8c6bc9/bin /Users/user/.claude/plugins/cache/claude-plugins-official/superpowers/6.2.0/bin )
array pipestatus=( 0 )
prompt=''
array tied PSVAR psvar=(  )
undefined reswords
undefined saliases
array signals=( EXIT HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2 ZERR DEBUG )
integer 10 readonly status=0
undefined termcap
undefined terminfo
undefined userdirs
undefined usergroups
undefined watch
undefined widgets
array readonly tied ZSH_EVAL_CONTEXT zsh_eval_context=( cmdarg eval cmdsubst )
undefined zsh_scheduled_events so the routing test drives the real switch: an
adapter for an event name nothing routes is dead code.

Related #

Task: T020, T021
In a real 6-week export 1,480 of 1,564 outcome-bearing events succeeded, so the
green tick sat on 95% of rows and buried the 32 errors and 52 blocks among
identical-looking neighbours. Successful rows now carry no icon at all — a mark
means "look at this" — while VoiceOver still announces every outcome.

Policy blocks become rows for the first time: a block never dispatches, so no
tool_call record stands in for it and the menu could not show one. They qualify
on their outcome class (warnings and redactions do not, since the call's own
record represents those), read server:tool like the call they stopped, carry
the policy's reason as the row's second line, and take a triangle rather than a
differently-coloured circle so the difference survives greyscale.

Related #

Task: T022, T023
Every activity record except a policy decision already carried request_id, and
the tray glance reconciles its live SSE rows against the next poll of persisted
records by exactly that field. A block therefore rendered twice: once
provisionally from the event, once again from the record it produced.

EmitActivityPolicyDecision now takes the dispatch's request id and puts it on
the payload; the persistence subscriber copies it onto the record, so the two
share one identity. All 16 emit sites pass it — 13 in mcp.go, one in
mcp_routing.go, two through applyOutputSanitisation/applyOutputValidation whose
signatures gain the parameter.

Two sites had no id to pass. handleCallToolVariant minted its id below the
intent gates that can block, so minting moves above the earliest gate. Direct
mode reads the id off the transport context, which is empty for anything that
did not arrive over HTTP, so it falls back to a minted one — a transport id
still wins. Records written before this change have no request_id and stay
that way; nothing synthesises one for them (FR-015).

Related #

Task: T016, T017
MCP over HTTP is stateless: a session closes after 30 minutes of silence and is
only persisted once real work happens, so "currently connected" is not a
question the transport can answer. Listing only active sessions therefore left
the Clients section empty most of the day, telling the user nothing was
connected about a proxy three clients had used that morning.

GlancePresence classifies each retained session by time since its last
activity — active under 5 minutes, idle to 30 (the inactivity timeout,
inclusive at both ends), seen out to a 24-hour lookback — deduplicates per
client (name plus version, keeping the most recent session), orders by recency
and caps the rows at five. Sessions with no parseable timestamp are dropped
rather than guessed at, a future timestamp clamps to a zero age, and the summary
counts the whole deduped set rather than the five that got rows, with "seen"
deliberately uncounted.

Pure and AppKit-free, like GlanceSelection: three boundaries, a dedupe, a cap
and a count are not things to verify from a screenshot.

Related #

Task: T026, T027
The section asked the wrong question. It listed sessions whose status was
"active", but MCP over HTTP is stateless: a session closes after 30 minutes of
silence, so most of the day the list was empty and the menu said "No connected
clients" about a proxy three clients had used that morning. It was reported as
the most confusing part of the menu, and it was.

The tray now polls the whole retained session page unfiltered (the ordering by
last activity landed in T025, so the page is the most recently *used* sessions,
not the most recently started) and renders presence: a filled dot for a client
working now, a half-filled one for a client gone quiet with the time since it
was last heard from, a hollow ring for one from earlier today. Three shapes
rather than three colours, so the states survive greyscale.

The summary line follows: "2 active · 1 idle" over the whole deduped set,
counted past the five-row cap, with "seen" clients keeping their rows but
staying out of the headline — and the client segment disappearing entirely when
nobody is active or idle. The placeholder now says "No recent clients" and
appears only when nothing at all falls inside the 24-hour lookback.

GlanceSelection.activeClients is gone; its replacement is not a narrower filter
but a different question, and it lives in GlancePresence.

Related #

Task: T028, T029
The Activity (24h) entry answers "what has been happening?" in one glyph, and
it sat at the very bottom of the block — below every row it summarises, so the
user read the call-level detail on the way to the overview.

It now sits directly under the summary line, above the separator that opens the
detail: summary and shape are one block, the rows below are another. Nothing
about the submenu changes; only its position.

Related #

Task: T030, T031
The hand-written pipeline tests pin one rule at a time on three or four
records. This replays the committed reference fixture — 1,564 events from a real
working laptop, 52 blocks, 32 errors — one poll at a time, oldest event first,
over the same 100-record window the tray actually fetches, and asks what the
user would have seen at each of the 1,564 steps.

SC-001: no two adjacent rows ever share a group key, no count ever claims more
records than were fetched, and where a burst of 19 or more identical calls is
the newest thing that happened — ungrouped, it filled all five rows — it now
takes one, with the other tools in the window getting the rest.

SC-004: all 52 policy blocks become visible as rows at some step, and no run
ever mixes blocked records with ordinary calls. Before this feature that first
number was zero.

The fixture is read from specs/ by walking up from #filePath rather than bundled
as a package resource: it is the same 860KB file the spec quotes its numbers
from, and two copies would eventually disagree.

Related #

Task: T032
Spec 048 says opening the tray menu performs no network request, and the test
that claimed to check it built a counting data source, handed it to nothing, and
asserted it had not been called. It could not fail, whatever anyone added to
menuWillOpen.

AppController now has two seams, both small and both load-bearing. Its tray menu
lives on a TrayMenuHost (the NSStatusItem in production), so the real
menuWillOpen → rebuildMenu → in-place update → menuDidClose sequence can run
without putting an icon in the menu bar of whoever runs the suite. And its read
surface is injectable, defaulting to the live core client, so a fetch added to
any menu path has somewhere to be counted.

MenuOpenNetworkTests drives that sequence and asserts a zero delta at two
levels: the injected source, and the URL loading system under the APIClient a
regression might reach for instead. It pumps the run loop before asserting,
since every route to a fetch is async, and a second test proves the sequence
really did build the glance and rewrite it in place — otherwise deleting the
block would leave the zero-request assertion green.

Verified falsifiable: adding a glanceActivity fetch to menuWillOpen fails it.

Related #

Task: T033, T034
The prefix check already covers the exact title, and the pair fit on one
over-long line.

Related #

Task: T033
…telist

The projection's semantics were only described in code comments and a swagger
annotation, so a client author reading the feature doc had no way to learn that
exclude_payloads keeps intent.reason, decision, reason and client identity while
dropping everything else. Spell out the kept keys, why has_sensitive_data and
request_id survive, and where to go for the full record.

Also record the T035 gate results, regenerate ROADMAP.md for the completed
task list, and correct quickstart.md, which told the reader to run ./mcpproxy
without ever building it.

Related #
The core serialises an absent legacy LastActivity as the Go zero time,
"0001-01-01T00:00:00Z". That parses, so the start_time fallback never
fired and the client was silently dropped for being 2000 years old.
Timestamps at or before a year-2000 floor now count as missing.
Dumbris added 5 commits July 31, 2026 16:36
The minted id was UnixNano plus server and tool, so two calls blocked at
the same instant on the same tool shared an id — and the tray, which
collapses activity rows by request id, folded two attempts into one row.
A process-wide atomic counter is now appended; the existing prefix is
kept intact for log greppability.
The projection copied whitelisted keys regardless of value type, so a
record whose metadata.reason or intent.reason held an object or array
leaked that whole nested payload through the exclude_payloads boundary.
Non-string values are now dropped, and an intent left with nothing is
dropped with them.
The race test only fails when two mints genuinely land in the same
nanosecond, which is a property of the machine rather than of the code —
it can pass on a build with no disambiguator at all. Assert the numeric
final component advances on every mint, and that one counter is shared
across targets.
Fixing the shared helper left twelve call sites formatting their own ids
from the clock — internal tool requests (search_servers, retrieve_tools,
describe_tool, …), the call_tool dispatch id, and three ToolCallRecord
primary keys. Identical concurrent calls therefore still collided, and
the tray's collapse-by-request-id undercounted them.

mintCorrelationIDAt is now the only place in the package that turns the
clock into an identifier; a go/ast test enforces that so a new call site
cannot reintroduce the defect. Ids arriving from the transport context
are untouched.
The table read last_activity directly, so the core's zero-time sentinel
blocked the start_time fallback: rows rendered as "739997d ago" and sank
to the bottom of a list sorted by recency. It now asks
GlancePresence.lastActivity, which the tray already uses, so the two
surfaces cannot drift apart again.

The dedupe, sort and age buckets move to DashboardSessions so they can be
tested without a view; the sort key is a Date rather than a raw string.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2798d98
Status: ✅  Deploy successful!
Preview URL: https://a8810ca0.mcpproxy-docs.pages.dev
Branch Preview URL: https://090-tray-glance-v2.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

codecov-commenter commented Jul 31, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 78.94737% with 20 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/server/mcp.go 67.44% 12 Missing and 2 partials ⚠️
internal/storage/manager.go 85.00% 2 Missing and 1 partial ⚠️
internal/httpapi/activity.go 88.88% 1 Missing and 1 partial ⚠️
internal/server/mcp_code_execution.go 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: 090-tray-glance-v2

Available Artifacts

  • archive-darwin-amd64 (28 MB)
  • archive-darwin-arm64 (25 MB)
  • archive-linux-amd64 (16 MB)
  • archive-linux-arm64 (15 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (22 MB)
  • installer-dmg-darwin-arm64 (20 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 30681847505 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Live QA executed 2026-08-01 (manual protocol in specs/090-tray-glance-v2/verification/): 11/14 visual checks pass, 3 waived with unit coverage. All 47 CI checks green.

@github-actions
github-actions Bot merged commit b1d1bf6 into main Aug 1, 2026
54 of 55 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants