The key rode the wholesale settings→localStorage save, sitting at rest
readable by any same-origin script, while the AES-256-GCM key-vault built
for exactly this sat bypassed. Now: the key lives in a memory-only store
ref, persists only into the encrypted vault when a passphrase session is
active (migrating into the vault on unlock), and a one-time migration lifts
any existing plaintext key out of localStorage and re-saves the scrubbed
settings object immediately. Settings UI reports honestly how the key is
held. Typecheck clean; test suite unchanged (348 pass, 3 pre-existing fails).
Co-Authored-By: Claude <noreply@anthropic.com>
Browser-verified on archi-dev-box; all three were only visible by
driving the real UI.
The tab bar. `setArchyContent` put the node's grids up, then
`updatePanelFromText` replaced the bar with tabs inferred from the reply
text. "show me my own shared content" therefore landed on an "AI Brief"
— a prose restatement of the answer already on the left — with the
populated image grid no longer reachable. Guarding the panel arrays was
not enough: they held the right data while the tab bar had discarded the
way to see it. Archy tabs now lead, and the title follows the leading
tab. The prose stays; it just is not the only thing shown.
Tab order follows bucket size. A node with 13 photos and 2 tracks opened
on Songs and titled itself "2 Songs" for a 15-item answer.
Web search never worked embedded. `searchWeb` hardcoded
`/api/web-search` while every other call is built from BASE_URL. Under
`/aiui/` that asked the HOST for a path only the AIUI-scoped nginx
location serves, so it hit the node's API gate for a 403 and the CSP
refused the connection on top. Now BASE-relative. Additionally, the
embedded path skips the client-side search entirely: `streamViaArchy`
sends only the user's text, so the system prompt those results were
folded into is never transmitted — it was a round trip and a console
error per turn whose output provably reached no model. Web search for
the embedded path belongs node-side, with the other tools.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four defects, one visible symptom: a correct prose answer beside an
empty grid.
1. The assistant's curated RPC bridge had an arm only for
`content.list-mine`. `tools.rs` mapped the `peers`, `purchased` and
`films` scopes onto three real, dispatcher-registered handlers that
`assistant_dispatch_tool` had never heard of, so every non-"own"
scope died on its catch-all. Downstream that read as "the peers have
no content" — it was a missing match arm, and the tool never ran.
Regression test added: every scope the schema advertises must reach a
real handler.
2. `content.browse-all-peers` wrapped its whole fan-out in one
`timeout(..).unwrap_or_default()`, which DISCARDED every completed
batch the moment the budget expired. One slow peer turned a
partly-successful browse into "0 reached, 16 unreachable". Observed
live on archi-dev-box: back-to-back calls returned real peer items,
then nothing. Now accumulates per batch and checks a deadline between
them, so partial results always survive. Budget 20s -> 45s: two
batches of eight at a 10s per-peer timeout had no headroom at all.
3. `assistant.chat` returned only `{ text }`. The structured results of
any content tool the turn ran were dropped inside the loop, so the
surface had nothing to render. The turn now carries them through
(captured raw, before the untrusted wrap, since they go to a renderer
that treats every field as inert data, never back into the prompt).
4. The adapter classified images as 'excluded' and dropped them. A node
sharing mostly photos rendered as an empty grid while AIUI's image
grid sat unused. Images now have a bucket, with the paid-lock and
extension-fallback handling audio and video already had.
Also: the panel says "Loading…" while a turn is in flight and "Nothing
found" when it comes back empty, instead of leaving the previous
query's heading standing as though it answered this one; the system
prompt tells the model to call the content tool and summarise rather
than re-list what the cards already show; and a refused tool now names
its permission category so the trusted chrome can offer the settings
screen instead of leaving "I don't have a tool for that" as the only
clue.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regression from wiring the peers scope: requestArchyAllContent awaited all
three scopes together, so the grid waited on the slowest. `peers` browses every
federated node over FIPS (Tor fallback) and routinely takes tens of seconds or
times out when a peer is offline. On-device that read as
"content(peers) failed: Content request timed out" plus an AIUI that felt very
slow to open — with nothing rendered meanwhile, even though local content was
ready immediately.
Now `own` paints as soon as it lands and `owned`/`peers` fold in as they
arrive. A scope that times out costs only its own results.
Also records the operator's console findings as tasks: the `files` context
timeout, the web-search CSP block (13-09, now firing on every query), the
strfry icon 404, IndeeHub's relay.nostr.band socket, and the ask that `!archy`
over mesh be able to action container commands with text responses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Operator-reported via a Bitcoin-films transcript: "Banking on Bitcoin" was
captioned with *The Rise and Rise of Bitcoin*'s description, "Cryptopia" with
*The Bitcoin Standard*'s, and the section header "Documentaries:" bled into the
first card of each group. Read as the model talking nonsense; the model's prose
was correct throughout and only the pairing was wrong.
Several patterns anchor with `(?:^|\n)` so they fire only at a line start.
That makes m.index point at the NEWLINE — one character before the line the
match is really on — so extractDescriptionForTag's window, which walks back
from `matchIndex - 1`, landed on the PREVIOUS line. The description became
"previous line + this item's own text".
Normalised inside the helper rather than at each of its nine call sites, so a
pattern that gains a `(?:^|\n)` anchor later cannot silently reintroduce it.
Fault-injected to prove the tests are not vacuous: with the fix removed, two
fail with exactly the reported strings — 'Documentaries: – Early documentary
fo…' and 'The Rise and Rise of Bitcoin – Early …'. 26/26 with it restored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On-device, an empty films search looked like a broken fetch. The console said
only "library: not permitted" — the content scopes returned null without a
word, so an ungranted Media/File permission was indistinguishable from "this
node genuinely has no films". That ambiguity cost real diagnosis time and sent
me looking for a code fault that was not there.
Each scope now names itself when denied. The permission was the whole cause;
no content path was broken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported on-device: searching for films in AIUI returns nothing. Init only ever
asked for scope 'own' (content.list-mine — this node's own shared files), so
IndeeHub and everything else purchased, which lives in 'owned'
(content.owned-list), and other nodes' catalogs in 'peers' were never fetched.
Both scopes existed only as type-signature options with no call site anywhere
in the app.
requestArchyAllContent() now loads all three concurrently and merges once.
Merged rather than three setArchyContent calls because that sink REPLACES
films/podcasts — separate pushes would leave only whichever resolved last, the
same class of bug as the shared sequence guard fixed in aac81503. Deduped by
id, since a title can legitimately appear both owned locally and offered by a
peer. Each scope is caught individually so one dead or slow peer costs only its
own results, which is normal rather than exceptional.
requestArchyContent also stops clobbering songs with an empty array, mirroring
what requestArchyLibrary already did for films/podcasts.
vue-tsc clean, 3/3 useArchy tests pass, and the change is verified present in
the built bundle rather than assumed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three defects in the two query classifiers that pick the content tab and
its header label. Found while writing the regression test for the
"Podcast recommendations" mislabel the operator reported on-device.
- "recommend me 10 scifi films" matched NOTHING: the film rule listed
film|movie|movies but not the plural `films` — the operator's own
phrasing. It opened no content tab at all.
- "listen to a podcast" classified as `song`: the song rule's bare
`listen` was checked before the podcast rule. Specific terms now win —
podcast is matched first, and `listen to` is no longer a podcast token
(so "listen to music" stays a song query).
- A bare `show` counted as a podcast word, which is how an operator
phrases nearly everything ("show me my files", "show the logs"), so
unrelated queries rendered "Podcast recommendations".
Both classifiers are fixed identically and the reason they must agree is
now stated in each — they label the same panel. 16/16 content tests, 56/56
composable tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four defects found by on-device UAT, 2026-08-06.
1. D-08 persistence was WRITE-ONLY. chat() loaded the transcript only
AFTER the loop, to append — the model was never shown any of it. The
assistant answered "I don't have access to any previous conversation
history" with its own transcript on disk, and "and is it healthy?"
resolved to the node instead of the app just discussed. History now
replays into every turn (text only: a stale tool result must not be
re-presented as this turn's evidence), scoped by HistoryKey. The
replayed prefix is excluded from the append, or each turn would
re-persist the conversation and grow it geometrically.
2. The operator persona forbade the very answers the content surfaces
render. 13-01's prompt refuses anything without a matching tool, so
"recommend me 10 sci-fi films" was declined and the film/song/podcast
grids from 13-11 could never populate — two plans in contradiction.
The refusal rule now governs ACTIONS ON THE NODE; general questions
and recommendations are answered from the model's own knowledge.
(Whether the node should also SEARCH THE WEB depends on AIUI's
web-search setting, which embedded mode never forwards — captured as
a separate todo because it opens a new egress path.)
3. The content-surface loader labelled unrelated queries "Podcast
recommendations": the classifier matched a bare "show", which is how
operators phrase almost everything ("show me my files").
4. "Surfacing…" tracks at 0.2em and its final glyph collided with the
close button; the header now spaces them properly.
assistant::history 9/9 green incl. replay_feeds_prior_turns_back_to_the_model.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AIUI asks for the library the same way it asks for content, and the
fetch now actually fires without anyone typing a magic phrase:
- useArchy.ts: requestArchyLibrary(scope) sibling of requestArchyContent
(13-06), same bridge call with kind: 'library', routed through the
existing setArchyContent so the songs bucket fills exactly the way
films already does.
- init() now calls both requestArchyContent('all','own') and
requestArchyLibrary('own') once, fire-and-forget, immediately after
archyBridge.init() — the GAP-FOUND fix. 13-06 built the whole
content:request/content:push machinery and unit-tested it end to end,
but nothing in the live UI ever called it (13-06-SUMMARY.md's Known
Limitations); the fetch is now triggered by a real init-time UI event,
not merely callable.
- useContentPanel.ts's setArchyContent now also opens the panel and
populates availableTabs/activeTab/panelTitle when Archy supplied
non-empty content — previously only the data refs were set while the
tab bar and panelOpen stayed whatever the last regex-driven chat turn
left them, so real content could sit fully populated and still never
render. An empty bundle never force-opens the panel.
Deviation (Rule 2, mirrors 13-06's own archyBridge.ts precedent): kind:
'library' genuinely needs a different node-side RPC (music.list-tracks,
real tag-extracted metadata) than content.* (ContentItem has no artist/
album/duration field at all) — contextBroker.ts's handleContentRequest
gained one branch (fetchLibraryContent) to route it, and
aiui-protocol.ts's AIUIContentRequest.kind union gained the 'library'
literal, and archyBridge.ts's requestArchyContent kind param widened to
match. No second channel, no new message type, no new listener — the
existing content:request/content:push channel and its kind discriminator
carry this exactly as 13-06 designed it to. Full detail in the SUMMARY.
neode-ui: 926/926 tests green, vue-tsc -b clean. aiui: 341/344 (3
pre-existing, documented failures unrelated to this plan — 13-06/13-10
already recorded them), vue-tsc --noEmit clean.
Two more on-device UAT findings:
1. Deny-retry loop: the model, told 'the user declined', simply called the
tool again — each retry minted a fresh pending and re-opened the dialog
(T-13-50 habituation, mechanized). ToolExecCtx now remembers declined
actions for the turn, keyed by confirm::action_key — the same canonical
(tool_name, validated_args) identity the nonce binds — and execute_tool
refuses a re-ask before the gate, minting nothing. Regression test
declined_action_never_reprompts_same_turn.
2. Timeout chain: rpcClient's 15s default aborted every confirmable turn
client-side while the node kept the pending alive — the next turn then
re-announced it (modal over and over) and every wait read as 'timed
out'. assistant.chat now rides a 420s timeout; AIUI's bridge goes
180s→430s so the host's error path (which also expires the dialog)
always fires first. Declined ToolResult text now also tells the model
to stop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
build-aiui.sh (mid-write) + aiui/.gitignore, committed verbatim and UNVERIFIED
— not a task completion. The 13-09 executor will be killed by the reboot; its
continuation should read this checkpoint, judge it against the plan's
must_haves, and reset --soft / build forward as appropriate (same recovery
pattern as the 6ba52b22/13b576da broken-pipe rescue at the start of this phase).
Already committed by 13-09 before this: 6ac0ebbf (CSP sandbox task).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Operator-reported: keyboard opening in chat pads the tab bar and scrolls the
page instead of scaling the chat window. Triage: neode-ui already ships
interactive-widget=resizes-content + the --visual-viewport-height var, so
mobile-web Chrome resizes correctly — but an Android WebView ignores that meta
entirely, and the described pan-plus-padding is the adjustPan/edge-to-edge-
without-IME-insets signature. Companion-side fix documented for handover in
docs/companion-keyboard-viewport.md (manifest adjustResize, or IME insets when
edge-to-edge, plus a chrome://inspect verification recipe). Web side gets the
one real parity gap: AIUI's standalone index.html lacked the meta neode-ui has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Operator-reported: selecting a different item in the content window left the
context-surface banner showing the previous item's image. Root cause: detail
views are reused, not remounted, and useBannerFallback kept primaryIndex/
stage/apiUrl alive across the prop change — once stage hit 'api' or 'done' it
never re-evaluated. Reset is keyed on title + the primary URL set, with a
generation guard so an in-flight fetch for the old item cannot stamp its
artwork onto the new one. Heals Film/TVSeries/Book detail at once; 3
regression tests pin it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Operator-reported: the model-picker and conversation-menu overlays are hard to
read over busy chat content at path-glass-card's shared rgba(0,0,0,0.65).
Scoped .header-overlay-panel (0.88) in ChatHeader.vue only — path-glass-card
itself is untouched, so BookDetail/ArticleDetail/TVSeriesDetail/WebsiteDetail/
ContentPanel/ChatWindow keep their existing glass. Unlayered scoped rule beats
the @layer components class without !important, and reaches the panels through
their Teleport to body.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- archyBridge.ts: content:push case resolving the pending content:request
by id, and requestArchyContent(kind, scope) mirroring requestContext's
shape. Not in the plan's files_modified list, but required to satisfy
Task 3's own instruction to register the content:push handler on the
existing single bridge listener rather than adding a second
window.addEventListener('message') — see SUMMARY deviations.
- useContentPanel.ts: setArchyContent + archyContentActive; guards only
the panelFilms/panelSongs/panelPodcasts assignments inside
updatePanelFromText so Archy-sourced grids stay the source of truth
once populated, per plan scope. Books/TV/images/places/magazine/code/
recipes/news are untouched (13-PATTERNS.md: partial deprecation).
- useArchy.ts: requestArchyContent(kind, scope) calling
archyBridge.requestArchyContent then useContentPanel().setArchyContent.
No FilmGrid/SongGrid/NewsGrid/ContentGridView/content.ts edits (D-12).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the 'Talk to AIUI about it' path from the other side. archyBridge
gains a chat:prefill case behind its existing parent-origin validation, and
ChatInput prefills + focuses with the caret at the end.
Prefills rather than auto-sends: the operator sees and can amend the question
before it costs a model call, and a draft they had already started is never
clobbered by a background handoff. Auto-send is the natural seam for the
follow-up that actions things directly.
The bridge buffers a prefill that arrives before the composer mounts (collapsed
chat, mobile content tab) and replays it on registration, so a Cmd+K ask into a
cold frame is not silently dropped. onPrefill returns an unsubscribe so a
remounting composer cannot leak a stale handler.
Verified: vue-tsc clean; AIUI suite 332 passed. The 3 remaining failures
(seed-songs extraction x2, web-search system prompt) are pre-existing — I
confirmed by reverting 13-01's two AIUI files to their parent state and
reproducing the identical 3 failures without any phase-13 change present.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>