An ordinary chat message answers the session's open ask in the user's
own words, so keeping the form open would invite answers to questions
the conversation has already moved past. The form now closes as
cancelled, browsers clear the live card, and the model is told without
being woken so the notice rides into the turn the message itself
starts. Ignored duplicate queued messages skip the void on purpose:
they must not void an ask posted after the queued original.
Documentation should describe what the software does, not enumerate
what it does not do. Rephrase the data-directory wording around the
positive facts (each directory is independent; a new root starts with
empty state) and drop the "never moves or copies" constructions from
config.md, the config.html env table, and the changeset.
Changing PI_WEB_DATA_DIR selects where managed state lives but never
moves or copies existing state. Explain how to carry session archives
over manually, and fix the stale "moves managed state location" claim
in the config.html env table.
Creating or opening a session could stall for reasons the daemon knew
about and never shared. The browser invented the whole message it showed
while waiting -- "Creating session: Waiting for the backend session to be
ready" -- which says that we are waiting but never what for. A shared
ModelRuntime read during startup can be handed a network refresh that is
already in flight, and extensions may do their own network I/O while
loading, so the wait is real and previously unattributable.
The pre-session gap turned out to be a missing shared key rather than a
missing channel: publishActivity needs the PiAgentSession being built, but
the session id and cwd are both known before the first await. So create()
now publishes a new global session.startup event carrying an ordinary
SessionActivity, routed by cwd -- the one identity a browser row waiting
for a session id can match, since the client-invented pending id is
unknown to the daemon and the daemon's id is unknown to the browser.
Two phases are reported, each published before the await it describes so
the label changes during the wait rather than after it: "Starting the Pi
session" and "Loading session extensions". Both are facts, because the
service awaits exactly one call for each. A concurrent background catalog
refresh is appended as a note ("provider model lists are refreshing"),
never as the cause: the refresher can prove a refresh is running but not
that this startup joined it. ModelCatalogRefresher gains only a read-only
isRefreshInFlight() getter; cadence, timeout, and coalescing are untouched.
Reporting is event-only and synchronous. It writes no activities entry, no
workspace activity, and no unread state, so a failed creation leaves
nothing stranded, no await is added, and creation ordering and semantics
are unchanged. The window-ending idle report is skipped when a real
activity was published during startup, so an extension error survives.
The browser applies startup progress only when it can prove the target:
one non-discarded pending start in that cwd on the selected machine, or a
session whose id it already knows. A foreign workspace, another machine,
or two concurrent starts in one workspace keep today's generic wording
rather than showing one row the phase of another. An idle report restores
that generic wording, including the queued-messages variant.
docs/config.md said nothing a request triggers waits on a catalog fetch.
That is not strictly true for a refresh already in flight, so both it and
the generated docs/config.html now state the exception and say PI WEB
reports it while it happens.
The Pi extension provider baseline section stated that every later
provider registration is a no-op, naming session_start as an example.
That is now the exact case that is allowed, so the shipped configuration
reference contradicted the behavior. docs/config.md is in the package
files allowlist, so this text reaches users.
Describe what stays frozen, the two conditions under which a known
provider's model list refresh is applied, and why function-valued fields
are always rejected. Record the accepted trade-offs: catalogs are shared
daemon-wide state with last-registration-wins, and a model entry's own
baseUrl/headers take precedence over the provider-level values. Split
the log behavior out and note that ignored mutations are de-duplicated
per provider while applied refreshes are always logged.
Also correct the reload-behavior bullet: a restart is needed to add or
remove a provider or change its connection settings, but not for a known
provider refreshing only its model list.
The five-minute retry recovers from timeouts and unreachable providers,
but not from a provider that answers with an HTTP error status: pi
re-stamps checkedAt on that path, so the unforced retry lands inside a
fresh freshness window. Tracked in #100.
Finding 6: serialize createOfflineModelRuntime so overlapping calls cannot
interleave their PI_OFFLINE save/restore pairs and leave the process offline,
and name the process-wide visibility of that window in the docblock.
Finding 7: assert the offline construction through the public refresh seam via
reloadConfig() — the request path that regressed — instead of reading upstream's
private modelNetworkEnabled field.
Finding 8.4/8.5: document the background provider-catalog refresh in
docs/config.md and docs/config.html (cadence, timeout, single retry, offline
opt-out via PI_WEB_OFFLINE / PI_OFFLINE only), and update the changeset to
match the behavior after the earlier fixes.