fix(anchor): harden registry contract before wiring
This commit is contained in:
1 parent
53051ad4a3
commit
d516f9dd00
5 files changed
+283
-22
No files matched your search
@@ -17,6 +17,10 @@ streaming or rendering yet.
|
||||
- This slice advances RFC Phase 1 and Phase 2 together: it adds a local registry
|
||||
owner plus a shadow source-feed harness that can combine live, replay,
|
||||
settled, and in-flight observations into one anchor snapshot.
|
||||
- It also covers the RFC Phase 2.5 contract-hardening boundary: the semantic
|
||||
anchor seed excludes renderer presentation state, terminal states are exposed
|
||||
as constants with alias normalization, and replay + settlement ordering is
|
||||
pinned by tests before visible wiring begins.
|
||||
- The next independently reviewable boundary is Phase 3 settlement through the
|
||||
anchor owner. `S.messages`, `INFLIGHT`, stream-local state, and DOM nodes remain
|
||||
projection/cache layers until that wiring lands.
|
||||
@@ -72,9 +76,10 @@ the same event-envelope dedupe rule, and route events into one owner:
|
||||
|
||||
The registry may fill missing `run_id` / `stream_id` identity from the first
|
||||
matching normalized event, update lifecycle on terminal status, and copy the
|
||||
settled assistant message into `content.final_answer`. It rejects mismatched
|
||||
session or turn identity and skips duplicate live + replay observations by the
|
||||
same dedupe key.
|
||||
settled assistant message into `content.final_answer` as a derived render
|
||||
snapshot while keeping `content.final_message_ref` as the settled transcript
|
||||
reference. It rejects mismatched session or turn identity and skips duplicate
|
||||
live + replay observations by the same dedupe key.
|
||||
|
||||
This slice deliberately keeps the ownership boundary inert: `send()`,
|
||||
`attachLiveStream()`, replay hydration, `renderMessages()`, `S.messages`,
|
||||
@@ -89,6 +94,26 @@ feeds them through one local registry, and returns the resulting snapshot plus
|
||||
per-source apply results. This gives later slices an invariant target without
|
||||
making the current UI consume the owner yet.
|
||||
|
||||
Renderer-only UI state such as Compact Worklog expansion, Transparent Stream
|
||||
expansion, copy-button visibility, and scroll-follow preference is intentionally
|
||||
not stored in the anchor seed. Those choices belong in renderer state or a
|
||||
separate per-session UI preference store so replay and settlement do not carry
|
||||
historic display preferences as semantic facts.
|
||||
|
||||
`HermesAssistantTurnAnchors.terminalStates` exposes the RFC terminal-state enum:
|
||||
`completed`, `cancelled`, `interrupted`, `no_response`,
|
||||
`tool_limit_reached`, `compression_exhausted`, `connection_lost`, `degraded`,
|
||||
and `error`. `normalizeAssistantTurnAnchorTerminalState()` maps current source
|
||||
aliases such as `done`, `cancel`, `apperror`, `interrupted-by-user`,
|
||||
`max_iterations`, and `lost_worker_bookkeeping` into that enum.
|
||||
|
||||
During the later `INFLIGHT` migration, the registry is the semantic owner for
|
||||
event identity, lifecycle, final answer reference, and activity events.
|
||||
`INFLIGHT.lastRunJournalSeq`, `activityBurstAnchors`, `currentLiveSegmentSeq`,
|
||||
`streamId`, and cached live text/tool state remain recovery or renderer caches
|
||||
until the matching field is explicitly moved. The fallback order is journal
|
||||
replay first, settled transcript second, `INFLIGHT` only for gaps.
|
||||
|
||||
## Source Event Classification
|
||||
|
||||
Phase 0 classifies current sources before changing render behavior:
|
||||
@@ -118,3 +143,8 @@ The Phase 0 helper uses this order:
|
||||
|
||||
This mirrors the RuntimeAdapter Event Envelope and keeps the browser aligned
|
||||
with run-journal replay while the anchor registry is still unwired.
|
||||
|
||||
The registry tests also pin the reconnect/settlement race shape: if one run is
|
||||
observed live, replayed, and settled in either order, duplicate event envelopes
|
||||
are skipped and the resulting anchor has the same activity list, terminal state,
|
||||
final message reference, final-answer snapshot, and usage metadata.
|
||||
@@ -301,10 +301,6 @@ AssistantTurnAnchor
|
||||
artifacts[]
|
||||
side_effects[]
|
||||
usage
|
||||
presentation_state
|
||||
compact_worklog expansion state
|
||||
transparent_stream expansion state
|
||||
scroll/follow hints
|
||||
```
|
||||
|
||||
The anchor is not a backend schema requirement. It is the frontend model that
|
||||
@@ -335,6 +331,12 @@ renderers consume the anchor
|
||||
In the target model, stream `done` should not need to "make a different turn."
|
||||
It should complete the existing turn.
|
||||
|
||||
Renderer-only preferences are deliberately outside the semantic anchor. Compact
|
||||
Worklog expansion, Transparent Stream expansion, copy-button visibility, and
|
||||
scroll-follow preferences may live in renderer state or a per-session UI
|
||||
preference store, but replay and settlement must not persist those choices as
|
||||
assistant-turn facts.
|
||||
|
||||
## Anchor Creation And Identity
|
||||
|
||||
The normal creation path is:
|
||||
@@ -525,8 +527,8 @@ existing anchor:
|
||||
|
||||
1. verify that `session_id`, `run_id`, or `stream_id` still belongs to the
|
||||
anchor,
|
||||
2. write the final assistant answer into `anchor.final_answer` or bind
|
||||
`anchor.final_message_ref`,
|
||||
2. bind `anchor.content.final_message_ref` to the settled assistant message and
|
||||
write `anchor.content.final_answer` as a derived render snapshot,
|
||||
3. attach terminal state and usage metadata,
|
||||
4. merge settled reasoning/tool/artifact metadata into existing activity events,
|
||||
5. mark live-only events as settled or drop them if they were purely transient,
|
||||
@@ -542,6 +544,13 @@ Terminal state is separate from final answer text. A turn can have partial
|
||||
process prose, tool cards, reasoning, artifacts, or usage metadata and still
|
||||
fail to produce a normal final answer.
|
||||
|
||||
`content.final_message_ref` is the durable transcript reference when settled
|
||||
message identity exists. `content.final_answer` is not an independent semantic
|
||||
owner; it is a projection cache copied from the settled assistant message so
|
||||
early renderers can avoid chasing `S.messages` during every paint. If settlement
|
||||
later rewrites the transcript message, the anchor must be refreshed from that
|
||||
message instead of allowing the two copies to drift silently.
|
||||
|
||||
## Replay, Reload, And Reconstruction
|
||||
|
||||
Replay/reload should reconstruct the same Assistant Turn Anchor from durable
|
||||
@@ -569,6 +578,17 @@ anchor after `/api/chat/start` succeeds and returns `stream_id`. Reconstruction
|
||||
is for reload, reconnect, session switch, SSE replay, and recovery cases where
|
||||
the local anchor is missing or incomplete.
|
||||
|
||||
During migration, `INFLIGHT` remains a recovery cache until an anchor-backed
|
||||
field takes over. The handoff order should be:
|
||||
|
||||
| Current `INFLIGHT` field family | Anchor destination | Fallback rule |
|
||||
| --- | --- | --- |
|
||||
| `lastRunJournalSeq` / replay cursor | anchor event dedupe index plus run-journal cursor metadata | Prefer run journal replay; read `INFLIGHT` only to resume a missing browser cursor. |
|
||||
| `activityBurstAnchors` / live row anchors | `activity_events[]` identity and grouping metadata | Preserve DOM hints as renderer cache; do not let them outrank normalized events. |
|
||||
| `currentLiveSegmentSeq` / local live order | anchor-local source order when no Event Envelope exists | Use only as browser fallback identity; never dedupe by visible text. |
|
||||
| `streamId` / active transport key | `identity.stream_id` fallback below `run_id` | Do not hard-bind ownership to `stream_id` once `run_id` is known. |
|
||||
| cached assistant text / reasoning / tool state | `activity_events[]` and derived render rows | Use for reconstruction gaps only after journal and settled transcript evidence. |
|
||||
|
||||
`stream_end` deserves special care. It is a transport close signal and may
|
||||
trigger recovery. It must not be treated as proof that the turn completed.
|
||||
|
||||
@@ -651,6 +671,11 @@ replacement when no final answer exists. Transparent Stream should show the same
|
||||
terminal truth as the final chronological event. The two display modes must not
|
||||
disagree about the outcome.
|
||||
|
||||
Implementation code should expose the terminal states as constants rather than
|
||||
spread string literals across settlement, replay, and renderer code. Source
|
||||
aliases such as `done`, `cancel`, `apperror`, and runtime-specific error labels
|
||||
should normalize into this enum before renderer consumption.
|
||||
|
||||
## Artifacts And Side Effects
|
||||
|
||||
Agent turns produce more than prose and tool rows. They can create files, mutate
|
||||
@@ -765,6 +790,18 @@ changes should remain independently reviewable.
|
||||
- Keep Compact Worklog as the only visible renderer.
|
||||
- Add coverage proving all current event names have an explicit classification.
|
||||
|
||||
### Phase 2.5: Anchor contract hardening
|
||||
|
||||
- Keep renderer presentation state out of the semantic anchor seed.
|
||||
- Export terminal-state constants and normalize source aliases before Phase 3
|
||||
writes settlement outcomes.
|
||||
- Document which `INFLIGHT` fields become anchor-owned, which remain fallback
|
||||
cache, and which are renderer-only hints.
|
||||
- Define `final_message_ref` as the settled transcript authority and
|
||||
`final_answer` as a derived render snapshot.
|
||||
- Add order-invariance coverage for live + replay + settlement observations of
|
||||
the same run.
|
||||
|
||||
### Phase 3: Settlement through anchor
|
||||
|
||||
- Reconcile `done` into the existing anchor.
|
||||
@@ -832,7 +869,11 @@ An implementation should eventually satisfy:
|
||||
transport, or explicitly excluded,
|
||||
- replay/reconnect of one run is idempotent at the existing
|
||||
`(session_id, event_id)` dedupe ring: no activity event is dropped or
|
||||
duplicated when the same run is observed live and then replayed.
|
||||
duplicated when the same run is observed live and then replayed,
|
||||
- replay/reconnect and settlement are order-invariant for one run: whether
|
||||
replay events arrive before or after the settled assistant message, the anchor
|
||||
reaches the same final answer reference, terminal state, usage metadata, and
|
||||
activity event list.
|
||||
|
||||
## Review Checklist For Implementation PRs
|
||||
|
||||
|
||||
Reference in new issue
Block a user