Target end state. Upload a file / folder / image → it becomes Memory. Images: the system interprets the image and that description becomes a held assertion the Operator confirms (machine describes → proposes → human approves), through the same capture→confirm→commit grammar as a spoken note. Non-image files/folders: stored, and their content interpreted into a recallable description that also becomes Memory.
Headline. There are two image rails. The old one drafted a Memory assertion; the current one doesn't. That split — not a broken vision skill — explains both the "it worked before" recollection and the live "0%, no description" symptom.
Rail A — Phase 16 /contributions (older) | Rail B — Phase 58/59 /uploads (current chat/composer) | |
|---|---|---|
| Endpoint | POST /engagements/{id}/contributions | POST /engagements/{id}/uploads (uploads.py:441) |
| Image vision | describe_image_skill (skills/image_description.py) | image_vision_analysis (claude-sonnet-4-6) |
| Lands in Memory? | YES — creates a held Assertion + lineage (contributions.py:280) | NO — assertions_drafted=[] hardcoded (uploads.py:661) |
| Storage | local disk | MinIO / S3 object store |
The frontend chat/composer upload moved onto Rail B. Vision still fires, but its output stops at event grain and never becomes an assertion — a rail switch that dropped the Memory-landing, not a regression inside the vision code. Git history confirms image_vision_analysis was born complete (9405cb7), refined (516a005), and fixed not disabled (bd05d5c); no commit ever stubbed it or removed a key. A live upload on 2026-06-02 shows the vision call actually firing.
/chat's UploadAttachmentButton has a multi-file and a webkitdirectory folder picker; the composer's AttachButton is file-only by choice. Both → useEngagementUpload → postUpload → POST /engagements/{id}/uploads. Caveat: folder structure is flattened to basenames — the client sends file.name, not webkitRelativePath (uploads.ts:38)./uploads endpoint is code-complete (Phase 59 landed; tag phase-59-upload-pathway-completion): batch upload, 50 MB chunked cap → 413, rate-limited 10/min, detection registry, per-skill key resolver.MinIOObjectStore.put_blob (not a stub; auto-creates bucket). Default minio at localhost:9000. Dev caveat: empty default creds, no in-repo MinIO service — a live MinIO must be running or put_blob → 503.add_assertion → held → commit_assertion (_route_add_knowledge, router.py:255). Rail A already proves upload→assertion is possible.The "image_metadata 0% / image_ocr 0% / image_vision_analysis 0%, no description" picker is not skill confidence — skills have no confidence field (they emit reliability ∈ succeeded/unreliable/failed). Those percentages are the executor's chain-disambiguation heuristic (per_candidate_scores):
accompanying_message / purpose_declaration. With no message (or no keyword hit) it scores every candidate 0.0 and returns status="needs_clarification" before any skill runs (executor.py:211-219, 520-527).needs_clarification surface.Distinct from the key-gate: a missing Anthropic key would surface transformation_failed with an error message, not the 0% picker. Vision's key is DB-resolved via system_config.loomworks_llm_key / engagement key — not an env var. Two independent problems: (a) multi-candidate image routing dead-ends at 0% before running anything; (b) even past that, vision needs a DB-stored loomworks_llm_key.
assertions_drafted=[] hardcoded (uploads.py:661), zero add_assertion calls in uploads/, no listener converting upload_event_received into an assertion. Uploaded content lives only as an object-store blob + an upload_event_received row in memory_events — unreachable by recall (which reads committed assertions only). The model field and executor docstrings describe drafting held assertions — documented as intent, never wired.add_assertion→held→commit grammar, and Rail A's proven upload→assertion pattern (image_description.py) as the reference.loomworks_llm_key is populated for vision; the folder-structure flattening (if hierarchy matters).assertions_drafted → add_assertion), so an interpreted image/file description flows into the capture→confirm→commit loop and becomes recallable Memory. This is the one substantive missing piece; everything upstream already exists.Read-only. Nothing was modified. Grounding for scoping — no fixes, no design decisions.