The re-anchoring only re-dated days[], leaving every other date-bearing
piece stale (day tabs read the original M.days, hotel stays/places kept
September checkIns so the base-chip and covers() logic went wrong, crumb
title and version pill never re-rendered). Now apply_flight_anchors also:
- re-dates stays.checkIn/Out, places.dates and hotel booking strings
from the day segments (redateStaysAndPlaces)
- re-renders day tabs, base chip, crumb and version pill, and reads
tab/trip-rail labels from the live days
- persists immediately (force) — the one edit we must never lose
- stayRange derives the month instead of hardcoding 'Sep'
Plus the .eml fix: mail metadata headers embed field names in their
values (X-Arc: ...content-type:from:subject:...), which the old
unanchored case-insensitive match grabbed — so single-part MIME bodies
were never decoded and raw headers + QP escapes leaked into the model's
context. Header fields are now parsed line-anchored, last occurrence
wins. The user's three real .emls now extract to clean text.
Prompt: when a document shows only one flight leg, the model is told to
search the other attached documents for the missing leg before asking.
E2E: drop the real AA e-mail -> model finds the return leg in the other
attached doc, applies the anchors, and title/tabs/chip/stays all show
7–15 Nov, surviving a hard reload.
Context and documents now live on the server between turns (and across
reloads), per project, as requested:
mock/store/<trip>/docs/<docId>-<file> ORIGINAL uploaded bytes
mock/store/<trip>/docs/<docId>-<file>.txt extracted text (+ header)
mock/store/<trip>/chat.jsonl full LLM conversation
Endpoints: GET /trip-store/:trip (manifest incl. doc texts),
GET .../doc/:docId, GET .../chat (last 100 turns), POST .../doc,
POST .../chat. /parse-doc's extraction factored into shared finishParse()
so both paths use identical logic. The store dir is gitignored (PII).
Client: enterTrip rehydrates tripDocs + chatLog from the server
(loadTripStore, race-guarded, one-time migration of localStorage docs
from older builds), re-renders past turns in the chat panel, and
askLLM persists each turn fire-and-forget. localStorage now carries
only the plan state.
Verified end-to-end: drop 133 KB email → direct re-anchor, original +
parsed files on disk, both turns in chat.jsonl → full page reload →
doc chip, document and conversation restored from the server → question
asked without re-upload answered correctly (AK-77Q2ZD, 14C/22A).
Co-Authored-By: Claude <noreply@anthropic.com>
The real-email case exposed two gaps:
1. Confirmation friction: when the booked dates didn't match the plan,
the model asked to confirm instead of anchoring. The guideline now
says to apply directly (the user's ticket is the truth, and the edit
is undo-able) and report the date shift — verified: a dropped booking
now re-anchors in one round, and a redundant "yes" afterwards is
handled gracefully ("already done").
2. Amnesia on the follow-up: the model has no conversation memory, so a
confirming "yes" round had to re-derive the dates from the document
in context — and that copy was clipped at 20k chars. Now the recent
turns ride in the model's context (chatLog, ~12k char budget, upload
messages excluded since docSection carries the document), and the doc
is no longer clipped in-context at all (60k server cap ≪ 131k-token
window). A new guideline tells the model to apply a confirmed change
from its own earlier message rather than claiming it can't see info
it already quoted.
Extraction hardening: broader HTML detection (20k-char scan + tag
density), and a brute-force base64-block fallback in decodeMime for
mail clients with quirky MIME structure. The server now logs an
extraction preview and saves the exact model-facing text to
/tmp/parse-doc-last.txt for debugging real files.
Co-Authored-By: Claude <noreply@anthropic.com>
An 118 KB HTML flight confirmation exposed two problems:
1. Extraction — the old <[^>]+> tag-strip broke on a ">" inside a quoted
attribute (inline SVG data-URIs are common in mail markup) and the
text was silently cut at 20 000 chars, so the model saw a mangled,
truncated document ("it was truncated"). /parse-doc now uses a
quote-aware state-machine extractor (skips comments / MSO conditional
comments / style / script / head blocks; block tags → newlines; table
cells → " | "; single-pass entity decode), decodes .eml / MIME mail
(base64 + quoted-printable, html part preferred, one level of nested
multipart), collapses 10×+ repeated boilerplate lines, and caps at
60 000 chars with an explicit `truncated` flag (server logs it too).
File limit raised to 16 MB; Outlook .msg gets a clear "forward as
HTML" error. A 130 KB bloated fixture now extracts to 901 clean chars
with all flight details intact.
2. Memory — uploaded documents now stay attached to the trip: persisted
in tripStore with the plan, shown as chips in the chat, and included
in the model's context on EVERY turn, so follow-up questions
("what's my confirmation code?") are answered without a re-upload and
documents survive reloads. 📎 now works for any reference doc (flight
bookings still trigger the re-anchor flow; the model asks to confirm
when the booked dates don't match the plan).
Also: askLLM refuses to start a second agent loop while one is in
flight — a question typed during a long tool loop used to fork a
parallel loop that corrupted the first.
Co-Authored-By: Claude <noreply@anthropic.com>
The user drags a flight ticket (PDF/text/ics/html) onto the chat panel,
or uses the new 📎 button. The doc is text-extracted server-side
(pdftotext for PDFs; images rejected with a clear "no OCR" message),
then the LLM reads it, extracts the confirmed arrival + departure
(date, local time, airport, city) and calls a new deterministic tool,
apply_flight_anchors, which reconfigures the trip:
- re-dates the day window across the booked [arrival, departure] span
(contiguous when the day counts match, evenly spread when they don't)
- records the flights as hard-anchor bookings, replacing placeholders
- shortens the arrival day (start after landing + a real airport→hotel
taxi time) and the departure day (ends exactly at the booked time)
- trims stops that no longer fit and updates the trip title's range
Undo/redo and reload persistence now version the trip's bookings + title
alongside days/stays, so a re-configuration reverts and reloads cleanly.
Server gains POST /parse-doc. Verified E2E (LLM extraction + reconfigure),
idempotency, drag-and-drop, and undo; non-flight agent unaffected.
Co-Authored-By: Claude <noreply@anthropic.com>
server.js: /llm-status probe + /llm-chat proxy to the local llama.cpp
OpenAI-compatible server (192.168.3.7:1234, model ctx 131072, 2 parallel
sessions). Model name, base URL and token budget pinned server-side
(LLM_BASE/LLM_MODEL/LLM_MAX_TOKENS env to override); non-streaming, since
the hidden thinking text shares max_tokens with the visible answer.
app.js: checkLLM() alongside checkRouter(); free-form chat now goes to the
model with a compact live plan snapshot (bookings, stays, per-day stops
with times) as system context. Deterministic UI intents (discovery
drawer, time nudges, swaps) stay local — the LLM proposes, the UI
commits. Canned-reply fallback when the server is down or the model is
not loaded; status badge now reads e.g. 'online · router · llm'.
The mock can now work several trips at once: a trip picker in the
topbar, per-trip state in localStorage (edits + version history survive
switching), and a per-trip router key so each trip routes on its own
OSRM extract.
Colombia (Cartagena -> Santa Marta, 8 days) becomes the default working
trip, inside a new walking-profile OSRM instance:
- scripts/crop_pbf.py: streaming 3-pass PBF cropper (no PBF library;
the country file is too big for osmium extract -s complete_ways on
this box). Framing validated against osmium test fixtures and
osrm-extract. Drops relations (foot profile ignores restrictions).
- scripts/setup-osrm-colombia.sh: download -> crop (~4.6 MB) ->
extract/partition/customize with the official foot.lua -> serve
:5003. Verified: Getsemaní -> Castillo San Felipe 7.98 km / 96 min.
- server.js: ROUTERS map, ?router=colombia|northeast on /route,
/table, /router-status (per-router probe points).
- app.js: legs touching a transit stop are fixed (flight/taxi/bus via
M.multimodal), never re-routed; distance-0 router answers (both
points on one node) read as adjacent stops, not out-of-coverage.
- multi-hotel fix: day-scope map fit uses the stays covering THAT day,
so a two-city trip no longer fits the whole country per day.
Also: pre-cache-tiles.sh (Cartagena+Santa Marta z14-16, 2560 tiles),
tile cache write fix (Node rejects flags:'x'; buffer-then-write),
favicon, renderHotelMks load-order guard, tests updated for the new
default trip (all four puppeteer suites green, zero console errors).
- server.js: proxy /route, /table, /router-status to the local OSRM on :5000
- app.js: routeClient (polyline-5 decode, ordered route fetch); legs start as
straight-line estimates (conf 1) and are upgraded in the background to real
road geometry + duration (conf 3); out-of-coverage trips (Florence) fall
back gracefully with a 'estimate — outside router coverage' source
- L3 leg editor: dragging a waypoint now genuinely re-routes through the
router (was silently no-op — #app covered the map with pointer-events:auto,
so no map drag ever landed; #app is now pointer-events:none, children opt in)
- data.js: second demo city (Boston 14–16 Sep, inside the router's NE-US
extract) via ?city=boston; nudges + multimodal legs are now dataset-driven
- L0: Day/Trip scope toggle — per-day summary cards (pacing bar, stops,
end time, price, base) with click-to-jump; map fits the whole trip
- version history: commit() snapshots days+stays on every mutation; topbar
vN pill with undo/redo + dropdown timeline showing a diff label per version;
Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y; restore() re-enriches legs (snapshots may
predate route enrichment)
- offline: staged 'prepare for offline' card in the trip rail (itinerary →
POI details → vector tiles) that flips the topbar badge to '📦 offline ready'
- chat/scripts/nudges: Florence-specific strings generalized (stay name/dates,
vibes, pacing summary) so both demo cities read naturally