fix(aiui): content loads progressively — own first, peers when they arrive

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>
This commit is contained in:
archipelago
2026-08-07 03:08:03 -04:00
co-authored by Claude Opus 5
parent 086d381c4f
commit 05b459a65f
2 changed files with 85 additions and 38 deletions
@@ -108,3 +108,32 @@ touched only `appgate/mod.rs`; `routstr.rs` has no reference to appgate).
Per `RESUME-2026-08-06-media-loop.md`: fix → build → deploy to archi-dev-box → test
live → verify the shipped bundle changed → commit → push. No item is done without a
live check on the node.
## Added 2026-08-07 — operator console evidence
- **`content(peers) timed out` + AIUI slow to open** — CAUSED by the first version of
`requestArchyAllContent` awaiting 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. **FIXED**: progressive — `own` paints immediately, `owned`/`peers` fold in
as they land, a timeout costs only its own scope.
- **`files fetch failed: Context request timed out: files`** — the `files` CONTEXT category
(not content) is slow enough to hit the broker's deadline. Separate from the above; needs
its own look at what `sanitizeFiles()` does.
- **Web search blocked by CSP** — `connect-src http://<node>:*/aiui/` blocks
`/api/web-search`. Already recorded as 13-09's decision (the web-search setting must drive
the CSP node-side); now reproduced on every single query, so it is noisy as well as broken.
- **`strfry.png` / `strfry.svg` 404** — missing app icon.
- **`wss://relay.nostr.band` closed before established** — IndeeHub's own relay connection,
from its page. Separate from the local relay fix.
- **Operator ask: `!archy` over mesh must action commands.** The mesh AI should be able to
restart/stop/install containers and everything else that makes sense, with text responses —
i.e. the same tool surface the embedded assistant has, reachable from a mesh message.
## Assistant tool coverage (root cause of "I don't have a tool for that")
`content_list` mapped ONLY to `content.list-mine`, so asking about peer films got an honest
"I have no tool" — the model was right. Now takes `scope`: own | peers | purchased | films,
dispatching to content.list-mine / content.browse-all-peers / content.owned-list /
content.indeehub-projects. Peer browsing rides FIPS (`PeerRequest::new(fips_npub, onion,
"/content")`, 6s FIPS fast-fail then Tor), which is what the operator meant by "we use FIPS
for that" — the onion is the peer's identity, FIPS is the transport.
+56 -38
View File
@@ -270,55 +270,73 @@ export function useArchy() {
*/
async function requestArchyAllContent() {
if (!isInitialized.value) return
const scopes: Array<'own' | 'owned' | 'peers'> = ['own', 'owned', 'peers']
const results = await Promise.all(
scopes.map((s) =>
archyBridge
.requestArchyContent('all', s)
.then((res) => {
// Log the denial per scope. A silent null here is indistinguishable
// from "the node genuinely has no content", which is exactly the
// ambiguity that made an ungranted Media/File permission look like
// a broken films search on-device.
if (!res.permitted) {
console.warn(
`[AIUI Archy] content(${s}): not permitted — enable Media/File access in Archy Settings`,
)
return null
}
return res
})
.catch((err) => {
console.warn(`[AIUI Archy] content(${s}) failed:`, (err as Error)?.message ?? err)
return null
}),
),
)
const seen = new Set<string>()
// PROGRESSIVE, not all-at-once. The first version awaited all three scopes
// together, so the grid waited on the slowest — and `peers` browses every
// federated node over FIPS (falling back to Tor), which routinely takes
// tens of seconds or times out entirely when a peer is offline. On-device
// that showed up as "content(peers) failed: Content request timed out" and
// an AIUI that felt very slow to open, with NOTHING rendered in the
// meantime even though local content was ready immediately.
//
// So: paint `own` as soon as it lands, then fold in the slower sources as
// they arrive. A scope that times out costs only its own results.
const films: Film[] = []
const podcasts: Podcast[] = []
for (const res of results) {
if (!res) continue
const seen = new Set<string>()
const merge = (res: { films?: unknown; podcasts?: unknown } | null) => {
if (!res) return false
let added = false
for (const f of (res.films ?? []) as Film[]) {
const key = `film:${f.id}`
if (seen.has(key)) continue
seen.add(key)
films.push(f)
const k = `film:${f.id}`
if (seen.has(k)) continue
seen.add(k); films.push(f); added = true
}
for (const p of (res.podcasts ?? []) as Podcast[]) {
const key = `pod:${p.id}`
if (seen.has(key)) continue
seen.add(key)
podcasts.push(p)
const k = `pod:${p.id}`
if (seen.has(k)) continue
seen.add(k); podcasts.push(p); added = true
}
return added
}
const panel = useContentPanel()
panel.setArchyContent({ films, songs: panel.panelSongs.value, podcasts })
const paint = () => {
const panel = useContentPanel()
panel.setArchyContent({ films: [...films], songs: panel.panelSongs.value, podcasts: [...podcasts] })
}
const fetchScope = (scope: 'own' | 'owned' | 'peers') =>
archyBridge
.requestArchyContent('all', scope)
.then((res) => {
if (!res.permitted) {
console.warn(`[AIUI Archy] content(${scope}): not permitted — enable Media/File access in Archy Settings`)
return null
}
return res
})
.catch((err) => {
// A slow or absent peer is ordinary, not exceptional.
console.warn(`[AIUI Archy] content(${scope}) failed:`, (err as Error)?.message ?? err)
return null
})
// Local content first — it is fast and is what the operator sees instantly.
if (merge(await fetchScope('own'))) paint()
// Then the slower sources, each painting as it lands rather than blocking
// the others or the initial render.
await Promise.all(
(['owned', 'peers'] as const).map((scope) =>
fetchScope(scope).then((res) => {
if (merge(res)) paint()
}),
),
)
}
/**
* Request the node's real music library (13-11 — the D-13 wave) and hand
* it to the same `setArchyContent` sink `requestArchyContent` uses, so