Final sweep of the media-placeholder program: with all eleven producers
migrated, the consumer regexes are dead input and are hard-deleted —
media-understanding token un-mixing (bodies are pure user text now),
the persisted-user-turn placeholder drop, the session-memory strip, and
the inbound-event mediaPlaceholder field with its replacement branch
(an intentional removal of shipped SDK surface per the program's
hard-delete decision: plugins still minting placeholder bodies render
them as literal text until updated). formatMediaPlaceholderText remains
the single owner of the <media:kind> vocabulary, used only for
text-only carriers. Legacy tokens in pre-migration history remain
ordinary text.
The .sidebar-view-archived svg rule set stroke: currentColor but never
fill: none, so the archive icon's lid <rect> rendered with SVG's default
black fill — a solid black bar instead of the outlined Lucide archive
box. Align the rule with sibling sidebar icon styles (fill: none,
1.7px stroke, round caps/joins).
Final producer batch of the media-placeholder program: Telegram primary
bodies are caption-only with one aligned structured fact per native
media; the message-cache kind-parsing regex is deleted (native kind
stored directly); reply-chain, debounce/forward, group-history, and
ambient transcript lines render structured facts via the shared
formatter (removing the plugin-local duplicate of the caption-less
literal); audio-transcript and sticker-description replacements gate on
structured facts instead of exact placeholder strings. Also restores
unconditional failed-retryable recording when media resolution is
aborted for live updates, so shutdown cannot silently settle an
undispatched update.
* feat(channels): batch 3 producers drop media placeholder bodies
Media-placeholder program batch 3: iMessage, Signal, and WhatsApp stop
minting <media:kind> placeholder bodies. Primary bodies carry captions
only with one aligned structured fact per native attachment (type-only
for missing/rejected/oversized). Echo, quote, and pending-history
records gain structured media facts so dedupe and previews work without
rendered text. Signal media-only outbound sends drop the placeholder
body entirely (signal-cli accepts attachment-only sends; recipients now
see just the media). WhatsApp audio preflight de-stringed; qa-lab
sticker recognizer moved to structured facts; WhatsApp gif maps to
video (presentation mode, no behavioral consumer).
* test(whatsapp): keep media classifier private
Route automatic dashboard titles through the effective session model and auth profile while preserving explicit cross-provider utility ownership and the existing generic title fallback.
* fix(clickclack): split API base and refresh discussion provider
* chore: regenerate bundled channel config metadata and docs map for the apiBaseUrl key
* feat(system-agent): agentic cached caretaker greeting with quick actions
* test(gateway): pin caretaker greeting in session-ownership tests
* fix(system-agent): offer agent handoff chip only when a model is available
* fix(ci): typecheck narrowing, lint, and knip gates for greeting module
* fix(system-agent): reject structured greeting output on every retained line
* test(gateway): split caretaker welcome tests under the max-lines gate
* feat(anthropic): import Claude Desktop custom groups
* fix(anthropic): resolve Claude Desktop groups from LevelDB entries
Group discovery regexed whole decompressed blocks, so matches were not
attributable to a Local Storage value and byte order decided the winner.
On a real 5.4MB store that mislabelled 59 of 159 sessions, surfacing a
mojibake label spliced out of Snappy copy-record bytes.
Parse SSTable entries properly instead: prefix-delta keys bounded by the
restart array, newest internal sequence per user key (honoring deletions),
and record scanning confined to a single value. Values are normalized so
Chromium's UTF-16 form scans like Latin-1, and unflushed WAL writes keep
precedence over SSTables.
Prod LOC grows ~77; it buys structural correctness in place of ordering
luck, and folds the old index-only walk into one shared entry decoder.
Verified against the live store: 159 assignments, 0 mislabelled versus an
independent entry-level ground truth, 0 control-character labels.
* fix(ui): sort custom session groups ahead of project groups
Custom groups were pushed into the section list as encountered, so their
position depended on roster order rather than the documented behavior;
the existing test only passed because its fixture happened to be ordered
that way. Collect custom and project groups separately and concatenate,
and assert the guarantee with a reversed-input case.
* fix(anthropic): widen LevelDB fixture key type for test typecheck
The prefix-delta helper assigned a Buffer into a Buffer-typed accumulator
whose generic argument differed, which tsgo rejects in the test lane.
* chore(anthropic): drop release-owned changelog edit from the PR
CHANGELOG.md is generated at release time, so a normal PR must not carry
an entry. The release-note context lives in the feature commit message and
the PR body instead.
* fix(ui): make stale-chunk Reload survive the gateway restart
The 'A new version is available' banner appears precisely because the
gateway was just updated — so when the user clicks Reload, that gateway
is usually still restarting. The handler probed once and, on failure,
silently fell back to revalidating the same replaced chunk, so the
button looked dead and a manual hard reload was the only way out.
Poll the document probe until the gateway answers (bounded, so an
unreachable gateway still degrades to the recoverable panel error
rather than a fatal navigation), and show a disabled 'Reloading…' state
while waiting.
* fix(ui): enforce the reload wait deadline locally
The bound was only checked after each probe settled, so it relied on the
probe timing itself out. The default probe does self-abort, but a
caller-supplied one need not, and a probe that never settles would leave
the Reload button disabled forever. Race each probe against the
remaining deadline instead.
* fix(ui): satisfy lint and deadcode gates for the reload retry
CI caught two things local oxlint does not: the wait default returned
the timer id from a Promise executor (eslint no-promise-executor-return),
and retryStaleChunkReload became an unused export once the lazy-route
button moved to the polling variant.
Wrap the executor body, and collapse the two near-identical retry paths
into one: the polling helper with timeoutMs: 0 is exactly the old
single-shot behavior, which the stylesheet-recovery banner keeps.
* fix(ui): keep the reload wait bounded across interval waits
The deadline was checked before the interval wait, so a wait that
carried past it left the next iteration taking the unbounded
single-shot branch — a probe that never settles would then strand the
disabled Reload button past its own bound. Only the first attempt may
probe from outside the window.