Dropping an .xlsx into the composer did nothing: the accept filter silently
discarded it, and widening that filter alone would have killed the run, because
the pinned @ai-sdk/openai@3 converter accepts no document media type but
application/pdf and throws before a request is made.
Spreadsheets now work end to end:
- A dependency-free .xlsx / .csv reader (ZIP central directory + OOXML through
DecompressionStream and DOMParser). It decodes date serials via styles.xml so
a date does not render as 45231, and places cells by column reference so a
gap does not shift every later value under the wrong header.
- The sheet is rendered to a column-aligned table document. Comparisons of
table encodings put a markdown-style aligned table well ahead of CSV for
model comprehension, and the same document is what a human sees.
- buildPdf paginates. It emitted a single page and drew overflow past the
bottom margin, so anything after ~48 rows was silently absent from a
perfectly valid PDF. Single-page output is byte-identical, so the existing
golden assertions stand.
- A per-run bridge around the agent swaps the media type to application/pdf on
the model leg only, and restores the original type and metadata on the way
back. Both the composer chip and the transcript therefore read XLS with the
real filename. The bridge keys on the exact payload, so a genuine PDF
attachment is never mislabelled.
- onUploadFailed is wired. A rejected or unreadable file now says so instead of
vanishing with no trace.
The demo workbook is committed as a generator rather than a binary blob, so the
fixture exercises the real reader: shared strings, date-styled serials, a
second sheet, deflate.
Also moves @copilotkit/* to 1.71.0 and @ag-ui/* to 0.0.59, and deletes the
@ag-ui canary overrides from both pnpm-workspace.yaml and package.json. Those
existed only because a released @ag-ui/core rejected SUBAGENT_* events and
killed banking's harness console; stable 0.0.59 ships SUBAGENT_STARTED,
SUBAGENT_FINISHED and SUBAGENT_ERROR, and 1.71.0 depends on exactly 0.0.59, so
the overrides would now hold the tree behind its own dependency.
Verified on the running demo, not just in tests: gen UI renders, the subagent
harness completes the offsite run (14 rows, 6 merchants, $2,377.15, 1m 4s),
memory recall returns seeded memories, and a spreadsheet answers with a table
built from the real file. Lint, typecheck, 2494 unit tests and build all pass.
Checked the reskin skill for staleness: buildPdf was documented as single-page
and two passages said onUploadFailed is wired by nobody. Both corrected here.
reskinnable-demo was cloned from examples/showcases/banking and vendors an
identical Intelligence (durable-memory) docker stack — same seeded ids
(jordan-beamson / morgan-fluxx / northwind-demo-user), same INTELLIGENCE_API_KEY
and org. Both apps identified their backend purely by address, and both pointed
at http://localhost:7050. So with banking's stack already up, a bare `pnpm dev`
here (which, unlike run-demo.sh, checks no port bind) would silently attach to
banking's backend and read/write the SAME memory buckets — worst case, the
presenter reset button forgetting the neighbour demo's memories.
Shift every published host port by +200 (705x/715x -> 725x/727x) in lockstep
across compose, env, scripts, e2e config and tests so attaching to the wrong
stack is impossible by accident:
app-api 7050 -> 7250
gateway 7053 -> 7253
postgres 7156 -> 7256
redis 7158 -> 7258
minio API 7160 -> 7260
minio console 7161 -> 7261
bundled TEI 7167 -> 7267
The native Metal TEI on :7067 is left shared ON PURPOSE: it holds no demo state
(buckets live in the now-isolated postgres/redis), the same TEI version + model
yields byte-identical embeddings, and run-demo.sh reuses it when healthy rather
than forcing a second ~20x-slower model load. A comment records why.
Points the demo runner, smoke scripts and env template at the skin-scoped
routes and relocated modules, and gives the vendored Intelligence stack its own
compose project name.
That last one matters: cloned from banking, this app inherited the same project
name, so both stacks resolved to one Docker project sharing containers and
named volumes — a compose down in either destroyed the other's memory. The
smoke scripts also still posted to /agent/default/run, which no longer exists
now that agents are keyed per skin; nothing in the toolchain reads those files,
so no build or test could have caught it.