Skip to content

feat(discord-bot): offload >10MB attachments to Azure Blob - #395

Draft
omegent-app[bot] wants to merge 163 commits into
fork/devfrom
t3-discord/4777d441
Draft

omegent-app[bot] wants to merge 163 commits into
fork/devfrom
t3-discord/4777d441

Conversation

@omegent-app

@omegent-app omegent-app Bot commented Aug 11, 2026

Copy link
Copy Markdown

Problem

Discord rejects multipart uploads over ~10MB. Oversized agent attachments currently only get a "could not be attached" note with no download path.

Fix

When Azure storage is configured, files over the Discord limit are uploaded to a private, non-listable blob container under hard-to-guess paths (yyyy/mm/dd/{uuid}/{stem}-{random}{ext}), then linked with a read-only HTTPS SAS URL that expires in 3 days.

  • Config (optional): AZURE_STORAGE_CONNECTION_STRING (preferred) or AZURE_STORAGE_ACCOUNT_NAME + AZURE_STORAGE_ACCOUNT_KEY, container AZURE_STORAGE_CONTAINER (default discord-bot-attachments)
  • Markdown local-file refs rewrite to the SAS URL when offload succeeds
  • Fallback note still posts when Azure is unset or upload fails
  • Ops secrets example updated with the new env vars

Azure container setup (operator)

# private container (no public access / not listable)
az storage container create \
  --account-name <account> \
  --name discord-bot-attachments \
  --public-access off

Test plan

  • vp test run for azureBlobUpload, config, ResponseBridge
  • apps/discord-bot typecheck
  • With Azure creds staged: post a >10MB file from an agent turn and confirm SAS link works, then fails after expiry (or with stripped query)
  • Without Azure: oversized still posts the previous failure note

opened by patroza in chat thread Discord · Discord

github-actions Bot and others added 30 commits August 5, 2026 15:48
Add custom "Open with" applications

Source: tim-smart#4
Source head: 8c4bdfbc5b57f6b600233244d330f9efa41dc498
Source commits: 08e1a4fb949585c3c441d6d00455fe904f72cd7b,cd43a401c6c148f1fe26cff72104ac527ea189f3,a8370e7502c552ebb064436e42e1c00f86f0946b,8c4bdfbc5b57f6b600233244d330f9efa41dc498
Imported: complete product delta from the source PR.

(cherry picked from commit 9fae005)
Load direnv environments for provider sessions

Source: tim-smart#5
Source head: 8f5fc87c13f4628c179cda44d4f32f7fe4d316b2
Source commits: e4f07014d39964fde2498bcb35588974cc5e6232,0d1463af61e0bd174f698b2519ebf3b207a2eaca,a66e4160d5f4b79140ec8fbcbc6aa66af750a991,8f5fc87c13f4628c179cda44d4f32f7fe4d316b2
Imported: complete product delta from the source PR.

(cherry picked from commit 0da8bfe)
Add unsigned retry for commit signing failures

Source: tim-smart#6
Source head: 7d65c5a224e97a6b811b0a84892f1fda065c5963
Source commits: 18ee567ecfdb11c9372153127b26b5cf57213a76,72a6fae23c86708080c4fed346d5bf0f136f0221,6614b28239ed2330a8f601357a413f2d50da195a,ec169369daa554541511aa28f551b36f3dd26485,7d65c5a224e97a6b811b0a84892f1fda065c5963
Imported: complete product delta from the source PR.

(cherry picked from commit 03671a2)
Add /new command for contextual threads

Source: tim-smart#7
Source head: 2051a8003041fe2806fcb4bc7a0d8940579fc543
Source commits: 2051a8003041fe2806fcb4bc7a0d8940579fc543
Imported: complete product delta from the source PR.

(cherry picked from commit 4d94f31)
Add session dashboard board

Source: tim-smart#8
Source head: d9f8e4d0a8dc22231ca315f3c595c3597f3b13e5
Source commits: 268fb8df9863ffbda51b975a8dbe68f11c41500c,dde20f271f674da22dd8f3a08201c2acf5e58ee5,df4a145e7b2cd2dc17a7a595267d2d8eb0a2a3f0,ce5723ddb0bf630a18d4cb8227b5344d12626e72,ad8c1a6af41161e1fc38a52f681b306517c7b918,6281887e6125317da0c7b4252d59bfd41c9bf35e,550db6316c634febdbe1cb27334d1347c23c7b2a,d9f8e4d0a8dc22231ca315f3c595c3597f3b13e5
Imported: complete product delta from the source PR.

(cherry picked from commit cd0e281)
Recover interrupted provider turns after server restarts

Source: tim-smart#9
Source head: b181832560177250b90bbfe07b0882c9e5b93493
Source commits: 7f69028a25be21f1882ecba14b62f387ad60cf2a,1d52bce1376766d804ef884d7d50b8b6d1b48cf7,b181832560177250b90bbfe07b0882c9e5b93493
Imported: complete product delta from the source PR.

(cherry picked from commit 83de8f5)
Avoid repeated thread snapshot loads during subscription retries

Source: tim-smart#10
Source head: c8c9eadb9de3026706bc3a403ca05b12d0da8dd5
Source commits: c8c9eadb9de3026706bc3a403ca05b12d0da8dd5
Imported: complete product delta from the source PR.

(cherry picked from commit 9e400c3)
Add image upload button to compact chat composer

Source: tim-smart#11
Source head: 1ff63f9b9c418ef56a46c6422d22c01de97581a8
Source commits: 1ff63f9b9c418ef56a46c6422d22c01de97581a8
Imported: complete product delta from the source PR.

(cherry picked from commit 720ec65)
Truncate mobile branch toolbar controls

Source: tim-smart#12
Source head: 1b7d44428472511bc98d8f936654359ce2536901
Source commits: 1b7d44428472511bc98d8f936654359ce2536901
Imported: complete product delta from the source PR.

(cherry picked from commit dc2bbb4)
Clean up worktrees when archiving threads

Source: tim-smart#13
Source head: a23f42d6ac671ea36b8db5d03934c089a31be448
Source commits: 4a194707ed134f993502ac5fdf36a8425f1769cd,1b6688aa5b641010cb2e9dad23d36d87257403ad,9ed32aa3923fb674380564b1ffcb3268290069b9,a23f42d6ac671ea36b8db5d03934c089a31be448
Imported: complete product delta from the source PR.

(cherry picked from commit 7e02dc9)
Pass hosted app channel into Vercel web builds

Source: tim-smart#14
Source head: de6966a6784b4703145c20b84fc482703bca4fa2
Source commits: de6966a6784b4703145c20b84fc482703bca4fa2
Imported: complete product delta from the source PR.

(cherry picked from commit 6333d8d)
Allow worktrees to reuse the selected branch

Source: tim-smart#15
Source head: 2d3900ba36c9397dc4fbe879c613a809f6b45384
Source commits: cd60531253fbafc470f5a5ac18d3e44832d3376d,2d3900ba36c9397dc4fbe879c613a809f6b45384
Imported: complete product delta from the source PR.

(cherry picked from commit 5e7dff2)
Add optional worktree removal confirmation

Source: tim-smart#16
Source head: c3f509fe8f690b704bb34692d9c132c0644db777
Source commits: 76f063e983ca3c39b20f79d8ea83783ab034251a,c3f509fe8f690b704bb34692d9c132c0644db777
Imported: complete product delta from the source PR.

(cherry picked from commit 9886109)
Stop retrying unavailable thread subscriptions

Source: tim-smart#17
Source head: 1359af8ba0b146e3d49f89b72c250f681e86199d
Source commits: 1359af8ba0b146e3d49f89b72c250f681e86199d
Imported: complete product delta from the source PR.

(cherry picked from commit 7b37a7a)
…nd; green tip

Bring Tim layer tip to typecheck green by joining main ref-refresh VCS client
state with fork failureKind/worktree-cleanup contracts, restoring
filterBrowseEntries/reuse-base-branch surfaces Tim dropped, and fixing
ChatView/Board call-site type errors left by incomplete Tim joins.

(cherry picked from commit 0e24917)
Bring fork/tim typecheck/test green after main pingdotgg#2679 + Tim client-runtime
rewrite: rejoin EnvironmentSubscriptionRpcTag/localApi/ws scopes, wire
BackgroundPolicy/ResourceTelemetry layers, force openpgp for signing tests
on hosts with gpg.format=ssh, and treat TRACE2 child_exit without
child_class as hook finish (git 2.55+).
"work" is no longer an empty full-catalog query once Worktree remove
confirmation is searchable. Keep the word-wrap false-positive check and
assert the worktree setting is the sole full-catalog hit.
Source: pingdotgg#4018
Source SHA: de8fd65

Imported: bounded server activity snapshots, cursor pagination, lazy web history loading, reconnect-safe reset/dedup, and disabled eager browser sidebar hydration.
Adapted: preserved Tim thread lifecycle handling and Omega composer/minimap behavior while resolving current-stack conflicts.
Excluded: none of the source PR behavior; native mobile pagination remains separate because pingdotgg#4018 intentionally excludes it.
…#3510) (#35)

Source: pingdotgg#3510
Source SHA: 034f4936d7a1435887bb62ac3f2db61f08928cbf

Imported: native mobile lazy loading for older thread activity, a 1,000-event subscription catch-up ceiling with snapshot fallback, and synchronized stale snapshot watermarks.
Adapted: applied above the refreshed pingdotgg#4018 web/server candidate and preserved Tim lifecycle handling plus our mobile composer changes.
Excluded: pingdotgg#3510 server/web pagination duplicated by pingdotgg#4018, the later shared-hook refactor, formatting-only commits, and contract comments. The shared refactor can be revisited independently after production validation.
Source: pingdotgg#4176
Source SHA: 56b6615

Imported: O(1) command read-model maps, deleted-thread eviction, VCS cache cleanup, browser surface cleanup, preview idle TTL, and per-thread UI cleanup.
Adapted: preserved our thread settlement, snooze, and sequential worktree deletion actions while wiring upstream cleanup into the current hook.
Excluded: none.

Co-authored-by: Rusiru Sadathana <27785781+RusiruSadathana@users.noreply.github.com>
#44)

Source: pingdotgg#4506

Source SHA: f7eaa00

Imported unchanged as one candidate provenance commit.
…tgg#4558)

Imported from https://github.com/pingdotgg/t3code/pull/4558\n\nAdapted to retain our provider restart-recovery constants while replacing the local default-title check with the shared policy.
…gdotgg#4379) (#312)

Imported from pingdotgg#4379 at
a27510d060645809ae1472bba4dbb248dc624e25.

Open file previews revalidate on mount and subscribe to debounced
native filesystem watches so external edits (editors, git, agents)
show without a manual refresh.

Co-authored-by: omegent-app[bot] <306514130+omegent-app[bot]@users.noreply.github.com>
Co-authored-by: Enrico Polanski <16064771+enricopolanski@users.noreply.github.com>
Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
… (#328)

Imported from pingdotgg#5344 at source SHA
783fd02 (commits b623dc2 + 783fd02
squashed into one provenance commit).

Imported behavior:
- `reduceThreadStreamItems`, a pure reducer that folds a batch of thread
  stream items into one state and one persistable snapshot.
- `Stream.groupedWithin(64, 16ms)` on the live subscription so a burst of
  thread events publishes the `SubscriptionRef` once instead of per event,
  and web/mobile stop rebuilding large thread views per streamed event.
- `eventBatchSize` on `EnvironmentThreadStateOptions`, plus the upstream
  regression tests for ordered single-publication bursts and for persisting
  a settled snapshot when a batch ends with a non-persistable turn start.

Local adaptations:
- Kept our `httpSnapshotLoadAttempted` guard around the HTTP snapshot
  fallback; the call now goes through `applyItems([...])`.
- Restored `setDeleted` (removed upstream) for the terminal
  `thread-deleted` subscription failure, which never reaches the item
  stream and so cannot go through the batch reducer. Cache removal is
  shared with the reducer path via `removeCachedThread`.

Excluded:
- `tasks/todo.md`, the author's scratch checklist.

Follow-up (fork/changes, not this layer): our `reload-required` branch and
`reloadFromServer` are built on the deleted `setThread`, so rebasing
fork/changes onto this layer must re-express them against the reducer
(split the batch at the reload point, then re-enter `applyItems` with the
remainder).

Co-authored-by: T3 Code PR Stack <41898282+github-actions[bot]@users.noreply.github.com>
patroza and others added 26 commits August 8, 2026 15:39
sync(upstream): merge upstream/main 0640410 into fork/dev
Adopts the three upstream commits after #381: a cross-environment usage page
reading provider transcripts (pingdotgg#5684), its chart fix (pingdotgg#5697), and one mobile
sheet for model and thread settings (pingdotgg#5625).

Resolutions:

- server.ts / ws.ts / client-runtime state: upstream's UsageService and its
  usageSummary atom family are additive next to the fork's diagnostics services
  (HostResourceProbe, ProcessResourceMonitor, TraceDiagnostics, BackgroundPolicy)
  and hostResourceSnapshot family — unioned.
- ThreadComposer: pingdotgg#5625 folds the model picker and provider options into a
  single settings sheet, replacing the fork's ControlPillMenu. The sheet is
  adopted, and the fork's usage signal rides on it: the trigger keeps
  ProviderUsageIcon with the live marker rather than upstream's plain
  ProviderIcon, so quota state stays visible at a glance. The fork-only
  collapsed-composer pill (upstream has none) now opens the same sheet instead
  of the retired menu.

Adversarial review caught that retiring the model menu also orphaned the fork's
numeric usage note: the marker survived on the trigger icon but the quota
percentage the menu rows carried had no home. It now hangs off the trigger
label, so both halves of the fork's usage signal survive the consolidation.
The plain ProviderIcon import went with upstream's replaced icon.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
sync(upstream): merge upstream/main a20923c into fork/dev
Adopts the 19 upstream commits that landed after 48aa875, headlined by the
project settings overhaul (pingdotgg#5768), per-project worktree/checkout selection
(pingdotgg#5766), sidebar drafts (pingdotgg#5777), manual project icons (pingdotgg#5775) and settle no
longer leaving monitors and dev servers running (pingdotgg#5774).

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
- mobile ProjectFavicon kept the fork's older loaded-URL set and orphaned
  upstream's projectFaviconCache module (request identity, disk cache,
  recycling key). Upstream's component is a strict superset now that it
  carries faviconPath, so it is adopted wholesale.
- upstream's new project settings page enforced single-owner only for
  runOnWorktreeCreate; the fork has three lifecycle hooks. It now uses the
  fork's clearConflictingLifecycleFlags, matching the chat header path.
- the empty-draft workspace reset lost the fork's reuseBaseBranch: false, so
  a stale reuse flag survived a reset that clears everything else.
- ClaudeAdapter granted the raw input.cwd while the query runs in the
  resolved cwd; the grant now names the same path.
- the script editor kept a delete confirm open across a request swap.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
merge: sync upstream through ba9c9ae (19 commits)
Adopts the four upstream commits that landed after ba9c9ae: the cross-platform
mobile usage dashboard (pingdotgg#5743), which moves usageMerge/usageFormat into
@t3tools/shared, desktop route preservation during Clerk auth (pingdotgg#5770),
label-gated hosted-web preview deploys (pingdotgg#5465) and outline-styled theme buttons
(pingdotgg#5860).

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
## Summary

- Long-running T3 turn alerts in `#omegent-alerts` used a fixed
**10-minute** cooldown after the **15-minute** threshold, so a
legitimate multi-hour turn (or sticky `running` row) re-paged every ~10
minutes.
- Page instead on an **age-milestone ladder**: **0.25h → 0.5h → 1h → 2h
→ 4h → …** (`15m × 2ⁿ`) while `projection_turns.state = 'running'`.
- Alert body now shows current milestone and next rung; milestone state
is pruned when the turn leaves the long-running set. Bot restart
re-pages at most once for the current rung.

For the ~2h configurator turn that motivated this: previously ~11 pages
after 15m; now **4** (15m, 30m, 1h, 2h).

## Test plan

- [x] Unit tests for milestone math, between-rung silence, restart
re-page once, label formatting (`Alerts.test.ts`)
- [ ] Deploy / restart discord bot on guest; confirm a long turn pages
at ladder steps only
- [ ] Confirm finished turns stop alerting

Co-authored-by: T3 Code PR Stack <41898282+github-actions[bot]@users.noreply.github.com>
merge: sync upstream through 02f4ce5 (4 commits)
Follow-up to #385. This one-line change was pushed to the sync branch
**after** #385 had already
been merged at `841252ce6`, so it never reached `fork/dev`.

Upstream's pingdotgg#5465 runs the new hosted-web preview job on
`blacksmith-8vcpu-ubuntu-2404`. Every fork
workflow uses GitHub-hosted runners (`ci.yml` is `ubuntu-24.04` /
`macos-15` throughout), and
`web-preview.yml` was the only Blacksmith reference in the tree — so the
job would queue forever if
anyone ever applied the `preview:web` label.

The workflow is label-gated and also needs `VERCEL_TOKEN` /
`VERCEL_ORG_ID` / `VERCEL_PROJECT_ID`,
which the fork does not set, so it stays dormant either way. This just
removes the footgun rather
than leaving a job that cannot be scheduled.

Found by the grok-4.5 adversarial review of #385.

Co-authored by [@patroza](https://github.com/patroza)

opened by [Patrick Roza](https://discord.com/users/95218063095377920) in
chat thread **Discord** ·
[Discord](https://discord.com/channels/1083767712431480922/1534783738322485399/1534783738322485399)
· [T3](https://t3vm/?thread=584a9ad3-243e-4308-8a13-49acdd758b17)

Co-authored-by: omegent-app[bot] <306514130+omegent-app[bot]@users.noreply.github.com>
Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
Adds **Grok** and **Kimi** to the Usage overviews, web and mobile.
OpenCode is deliberately left
out — see the last section.

Both CLIs already write usage to disk, so this reads them exactly the
way the page already reads
Claude and Codex. No new telemetry, no provider APIs, no changes to how
anyone runs a turn.

## Where the numbers come from

| | Grok | Kimi |
| --- | --- | --- |
| File | `~/.grok/logs/unified.jsonl` — **one process-wide log**, all
sessions interleaved | `~/.kimi/sessions/<hash>/<session>/wire.jsonl` —
per session |
| Usage record | `shell.turn.inference_done`, one per model round trip |
`StatusUpdate`, one per served response |
| Model | **not on the usage record** — carried forward per session id
from `model changed` | **named nowhere at all** |
| Cost | priced: `grok-4.5` matches the LiteLLM table exactly |
unpriced, on purpose |

Two details that would be double-counting bugs if read casually:

- Grok's `prompt_tokens` is **inclusive** of `cached_prompt_tokens`, so
uncached input is the
difference, not the raw field. (Same trap Codex's `input_tokens` has,
handled the same way.)
- Grok's log is process-wide, so the model cannot be carried forward in
a scalar the way Codex's
per-session rollout allows — a single variable would credit one
session's turns to whichever
  session switched model last. It is carried **per session id**.

**Kimi is deliberately unpriced.** Nothing in Kimi's logs names a model,
and the CLI's *currently
configured* model says nothing about what served a turn three weeks ago.
Rather than attribute — and
therefore price — turns against a guess, they are recorded under a
sentinel that `usagePricing`
already treats as unpriceable. Kimi shows real token counts and an
honest `unpriced` share instead of
a fabricated cost. The page already surfaces that share in its
cost-quality panel.

## Validated against real data, not just fixtures

Ran the actual reader over this machine's actual state:

- **Grok: 573 records across 19 sessions, 80.2M tokens**, every one
attributed to `grok-4.5`. Model
carry-forward resolved **573/573** — `model changed` is emitted at
session start, not only on a
  switch, so no record goes unattributed.
- **Kimi: the one session that has a wire log — 17,729 tokens.** That
figure independently matches
Kimi's own `context.jsonl` counter (`17600` context + `129` output),
which is a real cross-check on
  the token math rather than a fixture agreeing with itself.

## Known limits — both upstream of us, neither hidden

- **Grok's log is a single file that is never rotated.** On this machine
it retains ~2.5 days
(`2026-08-07` → `2026-08-09`) while Grok sessions go back to July 15. So
a 30-day view will show
the full window for Claude/Codex and only the last few days for Grok.
Making this complete means T3
persisting its own snapshots over time — a real feature, deliberately
not smuggled in here.
- **Kimi writes `wire.jsonl` for only some sessions** (1 of 4 on this
machine), so its coverage is
  partial by construction.

## Why not OpenCode

Agreeing with your instinct, and there is a concrete blocker behind it:
the scanner only walks
`*.jsonl`, and OpenCode stores per-message JSON files rather than JSONL
rollouts. Supporting it means
a different traversal, not another parser.

## Contract version: deliberately *not* bumped

I bumped `USAGE_CONTRACT_VERSION` 3 → 4 first, and the review talked me
out of it. Adding a provider
literal is additive — an environment on an older server reports no
grok/kimi buckets and its payload
still decodes. Bumping would instead move that environment into
`staleEnvironments`, dropping **all**
of its usage including Claude and Codex, until every environment in a
fleet was upgraded. It also
does not protect an older client, which fails to decode `"grok"` before
any version check runs. So
the bump bought nothing and cost a real undercount; it is reverted, with
the reasoning recorded at
the constant.

## Adversarial review

Both reviewers were given the real record shapes as ground truth and
told to attack double-counting,
the cache/fingerprint model, session attribution, the sentinel, the
contract version, exhaustiveness,
and test integrity. **Neither substantiated a HIGH.** Four findings were
worth acting on; all are
fixed.

| Finding | Disposition |
| --- | --- |
| **grok-4.5, MEDIUM** — bumping the contract version drops a lagging
environment's **entire** usage, Claude and Codex included, not just the
providers it cannot report | **Fixed — bump reverted.** The reviewer is
right that an added literal is additive: an old environment reports no
grok/kimi buckets and its payload still decodes. And the bump does not
protect old clients either, since a payload containing `"grok"` fails to
decode before any version check runs. It bought nothing and cost a real
undercount across a mixed-version fleet |
| **gpt-5.6-sol, LOW** — a Grok turn whose session never announced a
model in that file is dropped, so a rotated log silently loses tokens |
**Fixed.** Those turns now record under an unpriceable bare-provider
sentinel, matching Kimi: the token count stays whole, the cost stays
honest. (grok-4.5 raised the same thing as its third follow-up) |
| **grok-4.5, MEDIUM** — Kimi's multi-turn and refresh semantics were
asserted nowhere | **Fixed.** Added tests locking both: distinct
responses sum because each re-bills its whole context, and a refreshed
status carries the same `message_id` so it collapses |
| **grok-4.5, LOW** — the day-breakdown empty state spans `colSpan={5}`
against a row that is now 7 columns | **Fixed.** Derived from
`PROVIDER_ORDER.length + 3` so it cannot drift again |
| **gpt-5.6-sol, LOW** — Grok's single append-only log defeats the
bounded scan cache: it is always fresh, so every append re-parses the
whole file | **Acknowledged, not fixed.** Real, and it is the same
property behind the retention limit above. Incremental byte-offset
parsing is the fix and is a feature in its own right. Worth noting the
file is self-limiting in practice — Grok truncates it, which is why it
only holds ~2.5 days |
| **grok-4.5, LOW** — Kimi's `context.jsonl` files are walked too |
Harmless: the substring gate skips them; only the skipped-file counter
sees them |

What both independently confirmed, having checked it against the real
logs rather than my
description of them: the Grok inclusive-cache arithmetic (gpt-5.6-sol
verified a real progression —
prompt `75,234`/cached `58,752` then prompt `76,314`/cached `75,136` —
that only makes sense if
`prompt_tokens` includes cached), the per-session model map under
interleaving, that a re-parse
replaces rather than accumulates cache entries, that multiple
environments scanning one `~/.grok`
de-duplicate to a single owner via the host+provider+path+volume
fingerprint, that the `kimi`
sentinel cannot collide with a real LiteLLM model through
slash-normalisation, and that the chart
test kept its regression value — a Claude value regressed to a
cumulative `30` still fails against
the expected `20`.

## Verification

- Recursive typecheck clean across all 18 packages — which is also what
proves the exhaustive
`Record<UsageProviderKind, …>` maps (labels, colours, order, marks, web
**and** mobile) were all
  updated rather than silently defaulting.
- **2,357 tests pass**, including 14 new parser tests written against
the real record shapes. The
only failure is the pre-existing `CodexTextGeneration` launch-args one,
which reproduces on
  unmerged `fork/dev`.
- `UsageProviderChart.test.ts`'s band assertion previously hard-coded a
two-element array, so it
broke on any new provider. Rewritten to assert what it was actually
protecting — that band values
are absolute rather than cumulative stack offsets — without pinning the
provider count.

Co-authored by [@patroza](https://github.com/patroza)

opened by [Patrick Roza](https://discord.com/users/95218063095377920) in
chat thread **Discord** ·
[Discord](https://discord.com/channels/1083767712431480922/1534783738322485399/1534783738322485399)
· [T3](https://t3vm/?thread=584a9ad3-243e-4308-8a13-49acdd758b17)

---------

Co-authored-by: omegent-app[bot] <306514130+omegent-app[bot]@users.noreply.github.com>
Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
Adopts the 20 upstream commits that landed after 02f4ce5, headlined by
project settings moving onto contextual project routes (pingdotgg#5923), settings and
usage breadcrumbs (pingdotgg#5929, pingdotgg#5930), the unified usage page chrome (pingdotgg#5823), a fix
for usage double-counting forked Codex sessions (pingdotgg#5887), SVG sandboxing
(pingdotgg#5916), and favicon resolution no longer pinning the event loop (pingdotgg#5538).

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
PROVIDER_ORDER was typed `readonly UsageProviderKind[]`, which accepts an
incomplete list. The `Record` maps beside it are exhaustive by their own type,
so adding a provider forced updates to labels, colours and marks — but not to
the order. A provider missing from the order still appears in the summary rows,
which come from `merged.providers`, while silently vanishing from the daily
columns, chart bands, legends and skeletons, all of which iterate the order.
The tests iterate it too, so they would preserve the omission rather than catch
it.

The order is now a `const` tuple with a type-level assertion that nothing in
UsageProviderKind is missing from it, on web and mobile both. Verified it fires:
adding a fifth literal to the schema produces "Type '\"probe\"' does not satisfy
the constraint 'never'" at the guard, next to the Record errors.

Found by the gpt-5.6-sol adversarial review.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
pingdotgg#5609 also added mobile-fingerprint-check.yml, which landed without a conflict
and so was never inspected. It runs on blacksmith-8vcpu-ubuntu-2404, which this
fork does not have, and it triggers on every pull request touching apps/mobile,
packages/client-runtime, packages/contracts, packages/shared, assets or
scripts. Its "Native fingerprint diff" check was already sitting queued on this
very PR and would never have reported — a permanently pending check, and a
blocked merge wherever branch protection waits on all of them.

Same fix as #386 for web-preview.yml: GitHub-hosted runners, which is what
every other fork workflow uses. There are now no Blacksmith references left in
.github/workflows.

Also drops the EllipsisIcon import the Sidebar conflict union left behind; the
project row's button is SettingsIcon after pingdotgg#5923.

Found by the grok-4.5 adversarial review.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
merge: sync upstream through 3d74474 (20 commits)
The Board read the same thread atom as the sidebar but never applied the
ownership filter, so a
sidebar filtered to **Mine** sat beside a board showing everyone's
threads. The board has no
ownership control of its own either, so there was no way to correct it
from the board.

## Why it needed more than a one-line filter

The obvious fix — read the stored value in `BoardView` and filter —
would have been half a fix. The
sidebar held the selection in **component state seeded from
`localStorage`**, so the board could
never observe a change: switching the filter in a sidebar rendered
*beside* the board would not have
reached it until a remount. Two surfaces on screen at once disagreeing
is the bug, not just the
missing filter.

So ownership is now one shared selection, because it is a user-level
*"whose work am I looking at"*
rather than a per-surface view:

- `useOwnershipFilter()` reads both keys through `useSyncExternalStore`,
subscribing to the
`storage` event **and** the same `LOCAL_STORAGE_CHANGE_EVENT` that
`useLocalStorage` already
  dispatches for same-tab updates. Sidebar and board now move together.
- `buildOwnershipPredicate()` is the single call site for
`threadMatchesMine`. Both surfaces filter
through it rather than each re-deriving the call — which is exactly what
let them drift.

**The raw storage format is preserved deliberately.** These two keys
hold bare strings (`mine`), not
JSON, predating `useLocalStorage`'s codec. Reusing that hook would have
decoded `mine` as invalid
JSON and silently reset every existing selection back to the default — a
quiet regression for anyone
who had chosen *Anyone* or *Theirs*.

## What changed

| File | |
| --- | --- |
| `components/ownershipFilter.ts` | new — the shared hook and predicate
|
| `components/Sidebar.tsx` | two `useState`+`localStorage` blocks and
two inline writes replaced by the hook; filtering routed through the
shared predicate |
| `components/board/BoardView.tsx` | applies the predicate in its
`threads` memo |
| `hooks/useLocalStorage.ts` | exports the same-tab change primitives so
a raw-format key can join the same channel instead of inventing a second
one |

## Verification

- Typecheck clean across 18 packages; `vp build` of the web app passes.
- **2,368 tests pass** (6 new), the only failure being the pre-existing
`CodexTextGeneration`
  launch-args one that reproduces on unmerged `fork/dev`.
- The new predicate tests pin the semantics rather than my assumptions —
I had two of them wrong
first time and corrected them against `threadMatchesMine`: attributed
work in an environment with
**no** identity claim reads as *Theirs*, not *Mine*, while a **fully
unattributed** thread stays
under *Mine* so local work does not vanish for anyone not using identity
claims.
- `forkSurfaceExistence` gains a guard asserting both surfaces still go
through the shared predicate,
  so the board cannot quietly lose it again.

## Two related divergences left alone

Found while tracing this, deliberately not folded in — say the word and
I will:

- The board's **project** dropdown persists under its own key
(`t3code:board:project-filter:v1`) and
  still does not follow the sidebar's project scope.
- **Snoozed** threads are shelved out of the sidebar's list but still
render as board cards.

Co-authored by [@patroza](https://github.com/patroza)

opened by [Patrick Roza](https://discord.com/users/95218063095377920) in
chat thread **Discord** ·
[Discord](https://discord.com/channels/1083767712431480922/1534783738322485399/1534783738322485399)
· [T3](https://t3vm/?thread=584a9ad3-243e-4308-8a13-49acdd758b17)

Co-authored-by: omegent-app[bot] <306514130+omegent-app[bot]@users.noreply.github.com>
Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
Board cards now show where a thread came from.

Both sidebars and **the mobile board** already mark a thread's source
and participants with
`ThreadIdentityMark`. The web board did not — so a card gave no way to
tell a Discord- or
Jira-started thread from a local one, or to see who else is on it,
without opening the thread. This
was a web/mobile parity gap rather than a new idea, so the placement is
copied rather than invented:
the mark trails the card title, exactly where `Sidebar.tsx`,
`LegacySidebar.tsx` and
`BoardScreen.tsx` put it.

## Layout

The title was `line-clamp-2 w-full`. It becomes the flexible child of a
new row
(`flex min-w-0 items-start gap-1.5`) with the mark as a `shrink-0`
sibling, so the two-line clamp and
truncation are unchanged and a long title cannot push the mark out of
the card.

**A thread with no source costs nothing**: `SourceChannelGlyph` returns
`null` for an empty channel,
so the row renders exactly as before — no reserved space, no height
change. That is asserted, not
assumed.

## Adversarial review

Both reviewers called the change sound. One real bug between them, plus
test-strength findings — all
fixed in `0bb062b5f`.

| Finding | Disposition |
| --- | --- |
| **gpt-5.6-sol, MEDIUM** — `BoardCardDragOverlay` is `aria-hidden` and
its test asserts it holds no focusable controls, but `ParticipantStack`
always renders `tabIndex={0}`. Dragging a thread **with participants**
put a tab stop inside a clone assistive technology cannot see |
**Fixed.** `ParticipantStack` / `ThreadIdentityMark` take an
`interactive` flag and the card passes its existing
`rendering.interactive` through. The existing overlay test passed only
because its fixture had no participants — a test agreeing with itself.
The new participant-bearing overlay case **fails without the fix**; I
verified that by reverting the prop rather than assuming |
| **both, MEDIUM/LOW** — the tests asserted the literal `Δ` character,
which would go quiet on any glyph redesign and says nothing about
non-Discord channels; no participant-path or overlay coverage |
**Fixed.** They now assert the component's own contract
(`source-channel-glyph` / `participant-stack` testids and the
`aria-label`), plus a participant-only fallback case and the overlay
case |
| **both, LOW** — the call site restated the participant-channel
fallback that `ThreadIdentityMark` already applies | **Fixed** in
`1d8f0eb50` |
| **grok-4.5, MEDIUM** — tabbing to the stack focuses something that
does not open the thread on Enter | **Not changed.** Pre-existing
`ParticipantStack` behaviour, identical in both sidebars; the board only
makes it denser. Worth fixing in the component, not smuggled into this
change |
| **both, LOW** — each participant-bearing card subscribes to the
identity-claim atom, and a board shows more cards than a sidebar shows
rows | **Accepted.** `O(visible cards)`, no per-card network, and claim
maps change rarely. Channel-only threads add no subscription at all |
| **grok-4.5, LOW** — the surface guard only proves the token appears in
the file | True of every guard in that file; it is an anti-stack-drop
net, not a behaviour test. The `BoardCard` tests carry the behaviour |

Both independently confirmed what I most wanted checked: the two-line
clamp and truncation still
hold, a card with no source is **byte-identical** to before (the glyph
renders `null`, so the flex
gap reserves nothing), the effective channel matches the sidebar's
derivation, `memo` is not broken,
and the fields really are populated in production — traced from
`useThreadShells()` through
`OrchestrationThreadShell` to the persistence query.

## Verification

- Typecheck clean across 18 packages; `vp build` of the web app passes.
- **2,368 tests pass**, the only failure being the pre-existing
`CodexTextGeneration` launch-args one
  that reproduces on unmerged `fork/dev`.
- Five new `BoardCard` tests: the Discord glyph appears for a thread
with a source, and **is absent**
for one without — the pair is what makes the first assertion mean
something.
- Confirmed the data is really there rather than test-only:
`OrchestrationThreadShell` carries
`originSource` and `participantSummaries`, and it is the same
`useThreadShells()` source the
  sidebar already renders the mark from.
- `forkSurfaceExistence` now guards the board card alongside the
sidebars it already checked.

> [!NOTE]
> The web unit suite has a pre-existing intermittent failure in
> `browserHistoryStore.test.ts` (~1 run in 3). It is **not** from this
branch: I reproduced it on
> clean `fork/dev` and again on `f067b34a1`, before any of my recent
work. Unrelated to the board,
> and worth its own look.

Co-authored by [@patroza](https://github.com/patroza)

opened by [Patrick Roza](https://discord.com/users/95218063095377920) in
chat thread **Discord** ·
[Discord](https://discord.com/channels/1083767712431480922/1534783738322485399/1534783738322485399)
· [T3](https://t3vm/?thread=584a9ad3-243e-4308-8a13-49acdd758b17)

---------

Co-authored-by: omegent-app[bot] <306514130+omegent-app[bot]@users.noreply.github.com>
Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
…391)

## Summary
Cross-client (app) user messages echoed into Discord used a plain `from
user@channel:` label, so they were easy to miss next to native Discord
traffic.

Prefix that echo label with 💭 so app-originated turns are obvious at a
glance:

`💭 from **patroza@desktop**:`

## Test plan
- [x] `vp test run apps/discord-bot/src/features/ResponseBridge.test.ts
-t formatEchoedUserMessage`
- [ ] Deploy/restart discord-bot and send a turn from the app; confirm
the Discord echo starts with 💭

opened by [joshuadima](https://discord.com/users/593167616273809448) in
chat thread **Discord** ·
[Discord](https://discord.com/channels/1083767712431480922/1536277042893693018/1536277042893693018)
· [T3](https://t3vm)

Co-authored-by: omegent-app[bot] <306514130+omegent-app[bot]@users.noreply.github.com>
Co-authored-by: Joshua Dimaunahan <170177550+MindfulLearner@users.noreply.github.com>
Adopts the 10 upstream commits that landed after 3d74474, headlined by the
multi-provider pull requests page with in-app reviews (pingdotgg#4849), themed
confirmation dialogs (pingdotgg#5624), built-in theme contrast (pingdotgg#6000) and the v0.0.33
release prep.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
pingdotgg#5624 replaced the desktop's native confirmation with a themed React dialog: it
removed `confirm` from DesktopBridge, the preload API and the IPC channel and
handler. The auto-merge had dropped the fork's `confirm` declaration while
keeping its implementation, and restoring it faithfully — as I first did — left
a service whose only remaining callers were its own tests, competing with the
themed dialog that actually runs. The whole surface goes: declaration,
implementation, error class and union entry, input type, button-index constant
and the tests. All confirmations now go through LocalApi.dialogs.confirm and
ConfirmDialogHost, which is upstream's intent.

The fork's flat/recency sidebar list never received pingdotgg#4849's right-panel PR
opening. Threading the required prop into the project rows was enough to make
the merge typecheck, so the gap was invisible: the recency rows called
openPrLink without a thread ref and always fell out to the browser. They now
take openPullRequestsInRightPanel and use the same contract as the project
rows — open in the panel, and navigate to the thread so the panel has something
to sit beside.

Found by the gpt-5.6-sol and grok-4.5 adversarial reviews respectively.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
merge: sync upstream through 0a7c662 (10 commits)
## Summary

- preserve horizontal padding when the VS Code server status bar becomes
visible
- add a source-level regression assertion for the VS Code surface

## Focused verification

- `vp test run apps/vscode/src/serverSystemInfoSurface.test.ts`

Co-authored-by: T3 Code PR Stack <41898282+github-actions[bot]@users.noreply.github.com>
Adopts the 11 upstream commits that landed after 0a7c662: unborn-HEAD VCS
status (pingdotgg#5944), a three-hour snooze option (pingdotgg#5914), persisted sidebar shelf
collapse state (pingdotgg#5136), Windows terminal and libc detection fixes (pingdotgg#5693,
pingdotgg#5354), OpenCode model parsing with slashes (pingdotgg#5072), and mobile/CI fixes.

Three conflicts, all small:

release.yml keeps the fork's GitHub-hosted runner — Blacksmith is not
available here — while taking upstream's timeout extension from pingdotgg#6034, which
is the actual fix that commit was for.

Sidebar.tsx: pingdotgg#5136 persists both shelves, but the fork already persisted the
settled shelf under its own key, so upstream's second declaration would have
been a duplicate. The fork's key stays (identical default, and switching would
reset every existing preference); upstream's snoozed-shelf persistence, which
the fork lacked, is adopted. Upstream's local SETTLED_TAIL_* constants are
dropped because the fork imports them from Sidebar.logic.

ThreadDetailScreen.tsx unions the imports.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
The comment came across with upstream's constant and claims the key is kept so
existing preferences survive a rename. That is true of the fork's settled-shelf
key, which does hold preferences, but not of this one: the fork had no snoozed
persistence before this merge, so there is nothing to migrate and the "v2" in
the key is upstream's own legacy spelling.

A comment that describes a migration nobody is doing is the kind of thing a
future merge reasons from, so it now says which key holds what.

Found by the gpt-5.6-sol adversarial review.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
merge: sync upstream through 9c7622d (11 commits)
… SAS

Discord rejects large multipart uploads. When Azure storage is configured,
oversized files go to a private non-listable container under hard-to-guess
blob paths, and the bot posts read-only HTTPS SAS links that expire in 3 days.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
@patroza
patroza force-pushed the fork/dev branch 4 times, most recently from 80434dc to fc6701e Compare October 3, 2026 10:00

This branch has not been deployed

No deployments
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