Merge pull request #4037 from franksong2702/franksong2702/stable-assistant-turn-anchors-slice3-registry

Add assistant turn anchor registry and shadow source harness
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-06-12 19:45:46 -07:00
commit d1a9e9ffbb
6 files changed
+1033 -36

No files matched your search

@@ -6,6 +6,25 @@ It documents the current per-turn state layers and the event-shape contract that
future anchor phases must consume. It does not claim that anchors are wired into
streaming or rendering yet.
## RFC Phase Progress
- The #3962 Phase 0 scaffold shipped through #3977 / v0.51.359: inventory the
current state layers, encode the owner seed, and pin the source classification
contract.
- PR #3980 / v0.51.366 delivered the first RFC Phase 2 foundation: normalize
current live, replay, and settled source events into anchor-shaped events while
staying unwired from rendering.
- 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.
## State Layers
| Layer | Current surface | Phase 0 anchor policy |
@@ -37,6 +56,64 @@ same event-envelope key. This is still inert: `send()`, `attachLiveStream()`,
`renderMessages()`, settlement restore, `S.messages`, `INFLIGHT`, and the DOM do
not consume the helper yet.
## Slice 3 Registry / Owner Skeleton
`HermesAssistantTurnAnchors.createAssistantTurnAnchorRegistry()` creates a local
owner object for one assistant turn. The registry contains the anchor seed, a
dedupe index, and application stats. It is not a global store and is not wired
into current runtime, session, or renderer code.
`HermesAssistantTurnAnchors.applyAssistantTurnAnchorSourceEvent()` and
`applyAssistantTurnAnchorSourceEvents()` normalize incoming source events, apply
the same event-envelope dedupe rule, and route events into one owner:
- `activity_events` for visible assistant activity such as prose, reasoning,
tools, control boundaries, and terminal status
- `artifacts` for workspace/file references
- `side_effects` for persisted state side effects
- `metadata_events` for settlement/session metadata such as `settled_message`
- `transport_events` for transport-only signals such as `stream_end`
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` 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`,
`INFLIGHT`, and DOM continuity still do not consume the registry. Later slices
can replace local renderer-owned state with this owner instead of adding another
parallel source of truth.
`HermesAssistantTurnAnchors.createAssistantTurnAnchorShadowSnapshot()` is the
shadow wiring harness for this slice. It accepts grouped `live_events`,
`replay_events` / `run_journal_events`, `settled_events`, and `inflight_events`,
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:
@@ -61,7 +138,13 @@ The Phase 0 helper uses this order:
1. `event_id`
2. `run_id + seq`
3. `session_id + local_id` as a browser fallback
3. `session_id + source_event_type + local_id + seq` as a browser fallback only
when a concrete local `seq` is present
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.
+48 -7
View File
@@ -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
+360 -15
View File
@@ -1,8 +1,8 @@
// Stable Assistant Turn Anchors scaffold (#3926).
//
// This file is intentionally inert: it defines the current ownership inventory,
// event classifications, and small pure helpers, but it does not register
// anchors or change any renderer. Later phases can wire these helpers into
// event classifications, and small owner helpers, but it does not register
// anchors globally or change any renderer. Later phases can wire these helpers into
// send(), attachLiveStream(), replay hydration, and renderMessages().
(function(){
const ROOT=(typeof window!=='undefined')?window:globalThis;
@@ -113,6 +113,40 @@
'excluded',
]);
const TERMINAL_STATES=Object.freeze({
completed:'completed',
cancelled:'cancelled',
interrupted:'interrupted',
no_response:'no_response',
tool_limit_reached:'tool_limit_reached',
compression_exhausted:'compression_exhausted',
connection_lost:'connection_lost',
degraded:'degraded',
error:'error',
});
const TERMINAL_STATE_ALIASES=Object.freeze({
completed:TERMINAL_STATES.completed,
complete:TERMINAL_STATES.completed,
done:TERMINAL_STATES.completed,
cancelled:TERMINAL_STATES.cancelled,
canceled:TERMINAL_STATES.cancelled,
cancel:TERMINAL_STATES.cancelled,
interrupted:TERMINAL_STATES.interrupted,
interrupted_by_user:TERMINAL_STATES.interrupted,
no_response:TERMINAL_STATES.no_response,
no_response_generated:TERMINAL_STATES.no_response,
tool_limit_reached:TERMINAL_STATES.tool_limit_reached,
max_iterations:TERMINAL_STATES.tool_limit_reached,
compression_exhausted:TERMINAL_STATES.compression_exhausted,
connection_lost:TERMINAL_STATES.connection_lost,
lost_worker_bookkeeping:TERMINAL_STATES.connection_lost,
degraded:TERMINAL_STATES.degraded,
error:TERMINAL_STATES.error,
failed:TERMINAL_STATES.error,
apperror:TERMINAL_STATES.error,
});
const UNSAFE_OBJECT_KEYS=Object.freeze([
'__proto__',
'constructor',
@@ -144,6 +178,20 @@
return typeof value==='string'?value.trim():'';
}
function _terminalStateKey(value){
return _cleanString(value).toLowerCase().replace(/[\s-]+/g,'_');
}
function normalizeAssistantTurnAnchorTerminalState(value, fallback){
const key=_terminalStateKey(value);
if(key&&_hasOwn(TERMINAL_STATE_ALIASES,key)) return TERMINAL_STATE_ALIASES[key];
const fallbackKey=_terminalStateKey(fallback);
if(fallbackKey&&_hasOwn(TERMINAL_STATE_ALIASES,fallbackKey)){
return TERMINAL_STATE_ALIASES[fallbackKey];
}
return null;
}
function _coercePayload(value){
if(value==null) return {};
if(typeof value==='string'){
@@ -242,14 +290,16 @@
}
function _statusForSourceEvent(sourceType, kind, payload){
const explicit=_cleanString(payload&&(payload.status||payload.state||payload.phase));
if(explicit) return explicit;
const explicit=_cleanString(_firstOwn(payload,['status','state','phase']));
if(explicit){
return kind==='terminal_status'
?normalizeAssistantTurnAnchorTerminalState(explicit,sourceType)||explicit
:explicit;
}
if(kind==='tool_started') return 'running';
if(kind==='tool_completed') return payload&&payload.is_error?'error':'completed';
if(kind==='tool_completed') return _own(payload,'is_error')?'error':'completed';
if(kind==='terminal_status'){
if(sourceType==='done') return 'completed';
if(sourceType==='cancel') return 'cancelled';
return 'error';
return normalizeAssistantTurnAnchorTerminalState(sourceType)||TERMINAL_STATES.error;
}
if(kind==='lifecycle_status') return 'running';
if(kind==='control_boundary') return 'pending';
@@ -280,7 +330,8 @@
if(runId&&seq) return 'run_seq:'+JSON.stringify([runId,seq]);
const sid=_cleanString(_own(event,'session_id'));
const localId=_cleanString(_own(event,'local_id'));
if(sid&&localId) return 'local:'+JSON.stringify([sid,localId]);
const sourceType=_cleanString(_own(event,'source_event_type'))||'event';
if(sid&&localId&&seq&&seq!=='pending') return 'local:'+JSON.stringify([sid,sourceType,localId,seq]);
return '';
}
@@ -374,6 +425,277 @@
return out;
}
function _copyObject(value){
if(!value||typeof value!=='object'||Array.isArray(value)) return {};
return {...value};
}
function _frozenIdentityCopy(identity){
const refs=Array.isArray(identity&&identity.source_message_refs)
?identity.source_message_refs.slice()
:[];
return Object.freeze({
..._copyObject(identity),
source_message_refs:Object.freeze(refs),
});
}
function _registryAnchor(registry){
return registry&&typeof registry==='object'&&registry.anchor&&typeof registry.anchor==='object'
?registry.anchor
:null;
}
function _registryContext(registry, context){
const anchor=_registryAnchor(registry);
const identity=anchor&&anchor.identity?anchor.identity:{};
return {
..._copyObject(context),
session_id:_cleanString(_own(identity,'session_id'))||_cleanString(_own(context,'session_id')),
turn_id:_cleanString(_own(identity,'turn_id'))||_cleanString(_own(context,'turn_id')),
run_id:_cleanString(_own(identity,'run_id'))||_cleanString(_own(context,'run_id')),
stream_id:_cleanString(_own(identity,'stream_id'))||_cleanString(_own(context,'stream_id')),
};
}
function _eventBelongsToAnchor(anchor, event){
const identity=anchor.identity||{};
const sessionId=_cleanString(_own(event,'session_id'));
const identitySessionId=_cleanString(_own(identity,'session_id'));
if(sessionId&&identitySessionId&&sessionId!==identitySessionId) return false;
const turnId=_cleanString(_own(event,'turn_id'));
const identityTurnId=_cleanString(_own(identity,'turn_id'));
if(turnId&&identityTurnId&&turnId!==identityTurnId) return false;
const runId=_cleanString(_own(event,'run_id'));
const identityRunId=_cleanString(_own(identity,'run_id'));
if(runId&&identityRunId&&runId!==identityRunId) return false;
return true;
}
function _ensureDedupeKeySet(eventIndex){
const existing=eventIndex.dedupe_key_set;
if(existing instanceof Set) return existing;
const set=new Set(Array.isArray(eventIndex.dedupe_keys)?eventIndex.dedupe_keys:[]);
Object.defineProperty(eventIndex,'dedupe_key_set',{
value:set,
enumerable:false,
configurable:true,
writable:true,
});
return set;
}
function _ensureRegistryShape(registry){
const anchor=_registryAnchor(registry);
if(!anchor) throw new Error('assistant turn anchor registry requires anchor');
registry.event_index=registry.event_index&&typeof registry.event_index==='object'
?registry.event_index
:{};
if(!Array.isArray(registry.event_index.dedupe_keys)) registry.event_index.dedupe_keys=[];
registry.stats=registry.stats&&typeof registry.stats==='object'?registry.stats:{};
registry.stats.applied=Number(registry.stats.applied)||0;
registry.stats.skipped_duplicate=Number(registry.stats.skipped_duplicate)||0;
registry.stats.skipped_excluded=Number(registry.stats.skipped_excluded)||0;
registry.stats.skipped_mismatched=Number(registry.stats.skipped_mismatched)||0;
if(!Array.isArray(anchor.metadata_events)) anchor.metadata_events=[];
if(!Array.isArray(anchor.transport_events)) anchor.transport_events=[];
_ensureDedupeKeySet(registry.event_index);
return anchor;
}
function _syncAnchorIdentity(anchor, event){
const identity=anchor.identity||{};
const runId=_cleanString(_own(event,'run_id'));
const streamId=_cleanString(_own(event,'stream_id'));
if(!_cleanString(_own(identity,'run_id'))&&runId) identity.run_id=runId;
if(!_cleanString(_own(identity,'stream_id'))&&streamId) identity.stream_id=streamId;
}
function _textFromContentValue(value){
if(typeof value==='string') return value;
if(Array.isArray(value)){
return value.map((item)=>{
if(typeof item==='string') return item;
if(!item||typeof item!=='object') return '';
const text=_firstOwn(item,['text','content']);
return typeof text==='string'?text:'';
}).join('');
}
if(value&&typeof value==='object'){
const text=_firstOwn(value,['text','content']);
return typeof text==='string'?text:'';
}
return '';
}
function _firstTextValue(...values){
for(let i=0;i<values.length;i+=1){
const value=_textFromContentValue(values[i]);
if(value.length>0) return value;
}
return '';
}
function _messageRefFromPayload(payload, event){
return _firstTextValue(
_firstOwn(payload,['message_id','id','local_id']),
_firstOwn(event,['local_id','event_id'])
)||null;
}
function _updateLifecycleFromEvent(anchor, event){
const lifecycle=anchor.lifecycle||{};
const createdAt=_own(event,'created_at');
const status=_cleanString(_own(event,'status'));
const kind=_cleanString(_own(event,'kind'));
if(!lifecycle.started_at&&createdAt) lifecycle.started_at=createdAt;
if((!lifecycle.status||lifecycle.status==='created')&&status==='running'){
lifecycle.status='running';
}
if(kind==='terminal_status'){
const terminal=normalizeAssistantTurnAnchorTerminalState(
status,
_own(event,'source_event_type')
)||TERMINAL_STATES.completed;
lifecycle.status=terminal;
lifecycle.terminal_state=terminal;
lifecycle.completed_at=createdAt||lifecycle.completed_at||null;
}
anchor.lifecycle=lifecycle;
}
function _updateContentFromMetadata(anchor, event){
const payload=_own(event,'payload')||{};
const sourceType=_cleanString(_own(event,'source_event_type'));
if(sourceType==='usage'){
anchor.usage=_copyObject(payload);
return;
}
if(sourceType!=='settled_message') return;
const role=_cleanString(_own(payload,'role'));
if(role&&role!=='assistant') return;
const finalAnswer=_firstTextValue(
_own(payload,'content'),
_own(payload,'text'),
_own(payload,'final_answer'),
_own(payload,'answer')
);
if(finalAnswer){
anchor.content=anchor.content||{};
anchor.content.final_answer=finalAnswer;
anchor.content.final_message_ref=_messageRefFromPayload(payload,event);
}
const usage=_own(payload,'usage');
const turnUsage=_own(payload,'_turnUsage');
if(usage&&typeof usage==='object') anchor.usage=_copyObject(usage);
if(turnUsage&&typeof turnUsage==='object') anchor.usage=_copyObject(turnUsage);
}
function _routeAnchorEvent(anchor, normalized){
const event=_own(normalized,'anchor_event');
const classification=_cleanString(_own(normalized,'classification'));
if(classification==='activity'){
anchor.activity_events.push(event);
_updateLifecycleFromEvent(anchor,event);
}else if(classification==='artifact'){
anchor.artifacts.push(event);
}else if(classification==='side_effect'){
anchor.side_effects.push(event);
}else if(classification==='metadata'){
anchor.metadata_events.push(event);
_updateContentFromMetadata(anchor,event);
}else if(classification==='transport'){
anchor.transport_events.push(event);
}
}
function applyAssistantTurnAnchorNormalizedEvent(registry, normalized){
const anchor=_ensureRegistryShape(registry);
const item=(normalized&&typeof normalized==='object')?normalized:{};
const event=_own(item,'anchor_event');
if(!event){
registry.stats.skipped_excluded+=1;
return Object.freeze({applied:false,reason:'excluded',normalized:item});
}
if(!_eventBelongsToAnchor(anchor,event)){
registry.stats.skipped_mismatched+=1;
return Object.freeze({applied:false,reason:'mismatched_anchor',normalized:item});
}
const dedupeKey=_cleanString(_own(item,'dedupe_key'))||assistantTurnAnchorEventDedupeKey(event);
const dedupeKeySet=_ensureDedupeKeySet(registry.event_index);
if(dedupeKey&&dedupeKeySet.has(dedupeKey)){
registry.stats.skipped_duplicate+=1;
return Object.freeze({applied:false,reason:'duplicate',normalized:item});
}
if(dedupeKey){
dedupeKeySet.add(dedupeKey);
registry.event_index.dedupe_keys.push(dedupeKey);
}
_syncAnchorIdentity(anchor,event);
_routeAnchorEvent(anchor,item);
registry.stats.applied+=1;
return Object.freeze({applied:true,reason:null,normalized:item});
}
function applyAssistantTurnAnchorSourceEvent(registry, input, context){
const normalized=normalizeAssistantTurnAnchorSourceEvent(input,_registryContext(registry,context));
return applyAssistantTurnAnchorNormalizedEvent(registry,normalized);
}
function applyAssistantTurnAnchorSourceEvents(registry, events, context){
const list=Array.isArray(events)?events:[];
return list.map((event)=>applyAssistantTurnAnchorSourceEvent(registry,event,context));
}
function _eventsForShadowSource(sources, primaryKey, fallbackKey){
if(!sources||typeof sources!=='object') return [];
const primary=sources[primaryKey];
if(Array.isArray(primary)) return primary;
const fallback=fallbackKey?sources[fallbackKey]:null;
return Array.isArray(fallback)?fallback:[];
}
function _shadowSourceContext(context, sourceLayer){
return {
..._copyObject(context),
source_layer:sourceLayer,
};
}
function createAssistantTurnAnchorShadowSnapshot(input){
const opts=(input&&typeof input==='object')?input:{};
const anchorInput=(opts.anchor&&typeof opts.anchor==='object')?opts.anchor:opts;
const sources=(opts.sources&&typeof opts.sources==='object')?opts.sources:opts;
const context=(opts.context&&typeof opts.context==='object')?opts.context:{};
const registry=createAssistantTurnAnchorRegistry(anchorInput);
const results={
live:applyAssistantTurnAnchorSourceEvents(
registry,
_eventsForShadowSource(sources,'live_events'),
_shadowSourceContext(context,'live')
),
replay:applyAssistantTurnAnchorSourceEvents(
registry,
_eventsForShadowSource(sources,'replay_events','run_journal_events'),
_shadowSourceContext(context,'replay')
),
settled:applyAssistantTurnAnchorSourceEvents(
registry,
_eventsForShadowSource(sources,'settled_events'),
_shadowSourceContext(context,'settled')
),
inflight:applyAssistantTurnAnchorSourceEvents(
registry,
_eventsForShadowSource(sources,'inflight_events'),
_shadowSourceContext(context,'inflight')
),
};
return Object.freeze({
registry,
results:Object.freeze(results),
});
}
function createAssistantTurnAnchorSeed(input){
const opts=(input&&typeof input==='object')?input:{};
const sessionId=_cleanString(opts.session_id);
@@ -407,26 +729,49 @@
activity_events:[],
artifacts:[],
side_effects:[],
metadata_events:[],
transport_events:[],
usage:null,
presentation_state:{
compact_worklog:{expanded:false},
transparent_stream:{expanded:false},
scroll:{follow:true},
},
};
}
function createAssistantTurnAnchorRegistry(input){
const anchor=createAssistantTurnAnchorSeed(input);
const registry={
identity:_frozenIdentityCopy(anchor.identity),
anchor,
event_index:{
dedupe_keys:[],
},
stats:{
applied:0,
skipped_duplicate:0,
skipped_excluded:0,
skipped_mismatched:0,
},
};
_ensureDedupeKeySet(registry.event_index);
return registry;
}
ROOT.HermesAssistantTurnAnchors=Object.freeze({
version:'slice2-normalizer',
version:'slice3-registry-shadow',
activityEventKinds:ACTIVITY_EVENT_KINDS,
stateLayers:STATE_LAYERS,
sourceEventClassification:SOURCE_EVENT_CLASSIFICATION,
classificationOrder:CLASSIFICATION_ORDER,
terminalStates:TERMINAL_STATES,
createAssistantTurnAnchorSeed,
normalizeAssistantTurnAnchorTerminalState,
assistantTurnAnchorEventDedupeKey,
classifyAssistantTurnAnchorSourceEvent,
normalizeAssistantTurnAnchorSourceEvent,
normalizeAssistantTurnAnchorSourceEvents,
createAssistantTurnAnchorRegistry,
applyAssistantTurnAnchorNormalizedEvent,
applyAssistantTurnAnchorSourceEvent,
applyAssistantTurnAnchorSourceEvents,
createAssistantTurnAnchorShadowSnapshot,
isAssistantTurnAnchorActivityKind,
});
})();
@@ -1,4 +1,4 @@
"""Slice 2 normalizer tests for Stable Assistant Turn Anchors (#3926)."""
"""Normalizer tests for Stable Assistant Turn Anchors (#3926)."""
from __future__ import annotations
import json
@@ -144,8 +144,9 @@ const inheritedContextIdentity = api.normalizeAssistantTurnAnchorSourceEvent({{
}}, inheritedContext);
const runSeqCollisionA = api.assistantTurnAnchorEventDedupeKey({{run_id:'a:b', seq:'c'}});
const runSeqCollisionB = api.assistantTurnAnchorEventDedupeKey({{run_id:'a', seq:'b:c'}});
const localCollisionA = api.assistantTurnAnchorEventDedupeKey({{session_id:'a:b', local_id:'c'}});
const localCollisionB = api.assistantTurnAnchorEventDedupeKey({{session_id:'a', local_id:'b:c'}});
const localCollisionA = api.assistantTurnAnchorEventDedupeKey({{session_id:'a:b', source_event_type:'tool', local_id:'c', seq:'d'}});
const localCollisionB = api.assistantTurnAnchorEventDedupeKey({{session_id:'a', source_event_type:'b:tool', local_id:'c', seq:'d'}});
const localNoSeqKey = api.assistantTurnAnchorEventDedupeKey({{session_id:'a', source_event_type:'token', local_id:'b'}});
console.log(JSON.stringify({{
version: api.version,
liveToken,
@@ -168,6 +169,7 @@ console.log(JSON.stringify({{
runSeqCollisionB,
localCollisionA,
localCollisionB,
localNoSeqKey,
}}));
"""
result = subprocess.run([NODE, "-e", script], text=True, capture_output=True, check=False)
@@ -180,7 +182,7 @@ def test_normalizer_maps_live_and_replay_to_same_anchor_event_identity():
live = data["liveToken"]
replay = data["replayToken"]
assert data["version"] == "slice2-normalizer"
assert data["version"] == "slice3-registry-shadow"
assert live["classification"] == "activity"
assert live["dedupe_key"] == 'event_id:"run-1:7"'
assert replay["dedupe_key"] == live["dedupe_key"]
@@ -316,15 +318,19 @@ def test_structured_fallback_dedupe_keys_do_not_collide_on_delimiters():
assert data["runSeqCollisionA"] == 'run_seq:["a:b","c"]'
assert data["runSeqCollisionB"] == 'run_seq:["a","b:c"]'
assert data["runSeqCollisionA"] != data["runSeqCollisionB"]
assert data["localCollisionA"] == 'local:["a:b","c"]'
assert data["localCollisionB"] == 'local:["a","b:c"]'
assert data["localCollisionA"] == 'local:["a:b","tool","c","d"]'
assert data["localCollisionB"] == 'local:["a","b:tool","c","d"]'
assert data["localCollisionA"] != data["localCollisionB"]
assert data["localNoSeqKey"] == ""
def test_slice2_normalizer_is_still_unwired_from_rendering_hot_paths():
def test_normalizer_and_registry_helpers_are_still_unwired_from_rendering_hot_paths():
helper_names = [
"normalizeAssistantTurnAnchorSourceEvent",
"normalizeAssistantTurnAnchorSourceEvents",
"createAssistantTurnAnchorRegistry",
"applyAssistantTurnAnchorSourceEvent",
"applyAssistantTurnAnchorSourceEvents",
]
for helper in helper_names:
assert helper not in _read(UI_JS)
@@ -48,12 +48,23 @@ const out = {{
layers: api.stateLayers,
classifications: api.sourceEventClassification,
classificationOrder: api.classificationOrder,
terminalStates: api.terminalStates,
terminalAliases: {{
done: api.normalizeAssistantTurnAnchorTerminalState('done'),
cancel: api.normalizeAssistantTurnAnchorTerminalState('cancel'),
apperror: api.normalizeAssistantTurnAnchorTerminalState('apperror'),
interruptedByUser: api.normalizeAssistantTurnAnchorTerminalState('interrupted-by-user'),
lostBookkeeping: api.normalizeAssistantTurnAnchorTerminalState('lost_worker_bookkeeping'),
maxIterations: api.normalizeAssistantTurnAnchorTerminalState('max_iterations'),
unknown: api.normalizeAssistantTurnAnchorTerminalState('unknown'),
}},
tokenKind: api.classifyAssistantTurnAnchorSourceEvent('token').kind,
streamEndClass: api.classifyAssistantTurnAnchorSourceEvent('stream_end').classification,
unknownClass: api.classifyAssistantTurnAnchorSourceEvent('unknown_future').classification,
eventIdKey: api.assistantTurnAnchorEventDedupeKey({{event_id:'run-1:2', text:'same'}}),
runSeqKey: api.assistantTurnAnchorEventDedupeKey({{run_id:'run-1', seq:2, timestamp:123}}),
localKey: api.assistantTurnAnchorEventDedupeKey({{session_id:'sid-1', local_id:'local-1', content:'ignored'}}),
localKey: api.assistantTurnAnchorEventDedupeKey({{session_id:'sid-1', source_event_type:'token', local_id:'local-1', seq:2, content:'ignored'}}),
localNoSeqKey: api.assistantTurnAnchorEventDedupeKey({{session_id:'sid-1', source_event_type:'token', local_id:'local-1', content:'ignored'}}),
zeroSeqKey: api.assistantTurnAnchorEventDedupeKey({{run_id:'run-1', seq:0, session_id:'sid-1', local_id:'local-1'}}),
nanSeqKey: api.assistantTurnAnchorEventDedupeKey({{run_id:'run-1', seq:NaN, session_id:'sid-1', local_id:'local-1'}}),
emptySeqKey: api.assistantTurnAnchorEventDedupeKey({{run_id:'run-1', seq:'', session_id:'sid-1', local_id:'local-1'}}),
@@ -146,10 +157,11 @@ def test_phase0_dedupe_prefers_event_envelope_not_visible_text_or_timestamps():
data = _anchor_api_snapshot()
assert data["eventIdKey"] == 'event_id:"run-1:2"'
assert data["runSeqKey"] == 'run_seq:["run-1","2"]'
assert data["localKey"] == 'local:["sid-1","local-1"]'
assert data["localKey"] == 'local:["sid-1","token","local-1","2"]'
assert data["localNoSeqKey"] == ""
assert data["zeroSeqKey"] == 'run_seq:["run-1","0"]'
assert data["nanSeqKey"] == 'run_seq:["run-1","NaN"]'
assert data["emptySeqKey"] == 'local:["sid-1","local-1"]'
assert data["emptySeqKey"] == ""
assert data["emptyKey"] == ""
helper_src = _read(ANCHORS_JS).split("function assistantTurnAnchorEventDedupeKey", 1)[1]
@@ -163,6 +175,30 @@ def test_phase0_dedupe_prefers_event_envelope_not_visible_text_or_timestamps():
assert "created_at" not in helper_src
def test_phase0_exports_terminal_state_contract_and_aliases():
data = _anchor_api_snapshot()
assert data["terminalStates"] == {
"completed": "completed",
"cancelled": "cancelled",
"interrupted": "interrupted",
"no_response": "no_response",
"tool_limit_reached": "tool_limit_reached",
"compression_exhausted": "compression_exhausted",
"connection_lost": "connection_lost",
"degraded": "degraded",
"error": "error",
}
assert data["terminalAliases"] == {
"done": "completed",
"cancel": "cancelled",
"apperror": "error",
"interruptedByUser": "interrupted",
"lostBookkeeping": "connection_lost",
"maxIterations": "tool_limit_reached",
"unknown": None,
}
def test_phase0_anchor_seed_matches_rfc_shape_without_registering_state():
data = _anchor_api_snapshot()
anchor = data["anchor"]
@@ -174,8 +210,7 @@ def test_phase0_anchor_seed_matches_rfc_shape_without_registering_state():
assert anchor["activity_events"] == []
assert anchor["artifacts"] == []
assert anchor["side_effects"] == []
assert anchor["presentation_state"]["compact_worklog"]["expanded"] is False
assert anchor["presentation_state"]["transparent_stream"]["expanded"] is False
assert "presentation_state" not in anchor
def test_phase0_inventory_doc_matches_scaffold_contract():
@@ -191,6 +226,6 @@ def test_phase0_inventory_doc_matches_scaffold_contract():
"Dedupe Invariant",
"`event_id`",
"`run_id + seq`",
"`session_id + local_id`",
"`session_id + source_event_type + local_id + seq`",
]:
assert marker in doc
@@ -0,0 +1,487 @@
"""Slice 3 registry tests for Stable Assistant Turn Anchors (#3926)."""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
ANCHORS_JS = REPO / "static" / "assistant_turn_anchors.js"
MESSAGES_JS = REPO / "static" / "messages.js"
UI_JS = REPO / "static" / "ui.js"
SESSIONS_JS = REPO / "static" / "sessions.js"
NODE = shutil.which("node")
def _read(path: Path) -> str:
return path.read_text(encoding="utf-8")
def _registry_snapshot() -> dict:
assert NODE, "node is required for assistant_turn_anchors.js registry tests"
script = f"""
const fs = require('fs');
const vm = require('vm');
const src = fs.readFileSync({json.dumps(str(ANCHORS_JS))}, 'utf8');
const sandbox = {{window:{{}}}};
vm.createContext(sandbox);
vm.runInContext(src, sandbox, {{filename:'assistant_turn_anchors.js'}});
const api = sandbox.window.HermesAssistantTurnAnchors;
const registry = api.createAssistantTurnAnchorRegistry({{
session_id:'sid-1',
turn_id:'turn-1',
}});
const results = api.applyAssistantTurnAnchorSourceEvents(registry, [
{{type:'token', data:'{{"text":"live token"}}', lastEventId:'run-1:1', created_at:'2026-06-11T00:00:01Z'}},
{{event:'token', payload:{{text:'replay token'}}, event_id:'run-1:1', seq:1}},
{{event:'reasoning', payload:{{text:'thinking'}}, event_id:'run-1:2', seq:2}},
{{event:'artifact_reference', payload:{{path:'answer.txt', kind:'workspace_file'}}, event_id:'run-1:3', seq:3}},
{{event:'state_saved', payload:{{kind:'memory', name:'session-state'}}, event_id:'run-1:4', seq:4}},
{{event:'stream_end', payload:{{}}, event_id:'run-1:5', seq:5}},
{{event:'done', payload:{{}}, event_id:'run-1:6', seq:6, created_at:'2026-06-11T00:00:06Z'}},
{{source_type:'settled_message', payload:{{role:'assistant', id:'message-final', content:'final answer', _turnUsage:{{input_tokens:8, output_tokens:13}}}}}},
], {{run_id:'run-1', stream_id:'stream-1'}});
const isolated = api.createAssistantTurnAnchorRegistry({{session_id:'sid-1', turn_id:'turn-2'}});
api.applyAssistantTurnAnchorSourceEvent(registry, {{
event:'token',
payload:{{text:'wrong session', session_id:'sid-2'}},
event_id:'run-1:7',
seq:7,
}}, {{run_id:'run-1'}});
console.log(JSON.stringify({{
version:api.version,
registry,
isolated,
results:results.map((item)=>({{applied:item.applied, reason:item.reason}})),
}}));
"""
result = subprocess.run([NODE, "-e", script], text=True, capture_output=True, check=False)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def _shadow_snapshot() -> dict:
assert NODE, "node is required for assistant_turn_anchors.js registry tests"
script = f"""
const fs = require('fs');
const vm = require('vm');
const src = fs.readFileSync({json.dumps(str(ANCHORS_JS))}, 'utf8');
const sandbox = {{window:{{}}}};
vm.createContext(sandbox);
vm.runInContext(src, sandbox, {{filename:'assistant_turn_anchors.js'}});
const api = sandbox.window.HermesAssistantTurnAnchors;
const shadow = api.createAssistantTurnAnchorShadowSnapshot({{
anchor:{{
session_id:'sid-shadow',
turn_id:'turn-shadow',
}},
context:{{
run_id:'run-shadow',
stream_id:'stream-shadow',
}},
sources:{{
live_events:[
{{type:'token', data:'{{"text":"live token"}}', lastEventId:'run-shadow:1', created_at:'2026-06-11T00:00:01Z'}},
],
replay_events:[
{{event:'token', payload:{{text:'replay duplicate'}}, event_id:'run-shadow:1', seq:1}},
{{event:'tool_complete', payload:{{tool_call_id:'tool-1', result:'ok'}}, event_id:'run-shadow:2', seq:2}},
],
settled_events:[
{{source_type:'settled_message', payload:{{role:'assistant', id:'message-shadow', content:'shadow final'}}}},
],
inflight_events:[
{{source_type:'inflight_snapshot', payload:{{status:'restoring'}}}},
],
}},
}});
console.log(JSON.stringify({{
version:api.version,
registry:shadow.registry,
results:Object.fromEntries(Object.entries(shadow.results).map(([key, value]) => [
key,
value.map((item)=>({{applied:item.applied, reason:item.reason}})),
])),
}}));
"""
result = subprocess.run([NODE, "-e", script], text=True, capture_output=True, check=False)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def _hardening_snapshot() -> dict:
assert NODE, "node is required for assistant_turn_anchors.js registry tests"
script = f"""
const fs = require('fs');
const vm = require('vm');
const src = fs.readFileSync({json.dumps(str(ANCHORS_JS))}, 'utf8');
const sandbox = {{window:{{}}}};
vm.createContext(sandbox);
vm.runInContext(src, sandbox, {{filename:'assistant_turn_anchors.js'}});
const api = sandbox.window.HermesAssistantTurnAnchors;
const toolRegistry = api.createAssistantTurnAnchorRegistry({{
session_id:'sid-tool',
turn_id:'turn-tool',
}});
const toolResults = api.applyAssistantTurnAnchorSourceEvents(toolRegistry, [
{{event:'tool', payload:{{tool_call_id:'call-1', name:'shell'}}}},
{{event:'tool_update', payload:{{tool_call_id:'call-1', text:'running'}}}},
{{event:'tool_complete', payload:{{tool_call_id:'call-1', result:'ok'}}}},
{{event:'token', payload:{{text:'first token'}}}},
{{event:'token', payload:{{text:'second token'}}}},
]);
const runRegistry = api.createAssistantTurnAnchorRegistry({{
session_id:'sid-run',
turn_id:'turn-run',
run_id:'run-a',
stream_id:'stream-a',
}});
const runMismatch = api.applyAssistantTurnAnchorSourceEvent(runRegistry, {{
event:'token',
payload:{{text:'wrong run'}},
event_id:'run-b:1',
run_id:'run-b',
seq:1,
}});
const streamAccepted = api.applyAssistantTurnAnchorSourceEvent(runRegistry, {{
event:'reasoning',
payload:{{text:'same run new stream'}},
event_id:'run-a:2',
run_id:'run-a',
stream_id:'stream-b',
seq:2,
}});
const identityRegistry = api.createAssistantTurnAnchorRegistry({{
session_id:'sid-freeze',
turn_id:'turn-freeze',
}});
identityRegistry.identity.session_id = 'mutated';
const metadataRegistry = api.createAssistantTurnAnchorRegistry({{
session_id:'sid-meta',
turn_id:'turn-meta',
}});
const inheritedPayload = Object.create({{
role:'assistant',
content:'inherited final should be ignored',
id:'inherited-message',
_turnUsage:{{input_tokens:99, output_tokens:99}},
}});
api.applyAssistantTurnAnchorNormalizedEvent(metadataRegistry, {{
classification:'metadata',
anchor_event:{{
session_id:'sid-meta',
turn_id:'turn-meta',
source_event_type:'settled_message',
local_id:'meta-1',
payload:inheritedPayload,
}},
}});
api.applyAssistantTurnAnchorNormalizedEvent(metadataRegistry, {{
classification:'metadata',
anchor_event:{{
session_id:'sid-meta',
turn_id:'turn-meta',
source_event_type:'settled_message',
local_id:'meta-2',
payload:{{
role:'assistant',
id:'message-structured',
content:[
{{type:'text', text:'structured '}},
{{type:'text', text:'answer'}},
],
usage:{{input_tokens:1, output_tokens:1}},
_turnUsage:{{input_tokens:8, output_tokens:13}},
}},
}},
}});
console.log(JSON.stringify({{
version:api.version,
toolRegistry,
toolResults:toolResults.map((item)=>({{applied:item.applied, reason:item.reason}})),
runRegistry,
runMismatch:{{applied:runMismatch.applied, reason:runMismatch.reason}},
streamAccepted:{{applied:streamAccepted.applied, reason:streamAccepted.reason}},
identityRegistry,
metadataRegistry,
}}));
"""
result = subprocess.run([NODE, "-e", script], text=True, capture_output=True, check=False)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def _race_snapshot() -> dict:
assert NODE, "node is required for assistant_turn_anchors.js registry tests"
script = f"""
const fs = require('fs');
const vm = require('vm');
const src = fs.readFileSync({json.dumps(str(ANCHORS_JS))}, 'utf8');
const sandbox = {{window:{{}}}};
vm.createContext(sandbox);
vm.runInContext(src, sandbox, {{filename:'assistant_turn_anchors.js'}});
const api = sandbox.window.HermesAssistantTurnAnchors;
function build(order) {{
const registry = api.createAssistantTurnAnchorRegistry({{
session_id:'sid-race',
turn_id:'turn-race',
run_id:'run-race',
stream_id:'stream-race',
}});
const live = [
{{event:'token', payload:{{text:'live token'}}, event_id:'run-race:1', run_id:'run-race', seq:1}},
];
const replay = [
{{event:'token', payload:{{text:'replayed duplicate'}}, event_id:'run-race:1', run_id:'run-race', seq:1}},
{{event:'tool_complete', payload:{{tool_call_id:'tool-1', result:'ok'}}, event_id:'run-race:2', run_id:'run-race', seq:2}},
{{event:'done', payload:{{status:'done'}}, event_id:'run-race:3', run_id:'run-race', seq:3, created_at:'2026-06-11T00:00:03Z'}},
];
const settled = [
{{source_type:'settled_message', payload:{{role:'assistant', id:'message-race', content:'race final', _turnUsage:{{input_tokens:5, output_tokens:8}}}}}},
];
api.applyAssistantTurnAnchorSourceEvents(registry, live);
if (order === 'replay-first') {{
api.applyAssistantTurnAnchorSourceEvents(registry, replay);
api.applyAssistantTurnAnchorSourceEvents(registry, settled);
}} else {{
api.applyAssistantTurnAnchorSourceEvents(registry, settled);
api.applyAssistantTurnAnchorSourceEvents(registry, replay);
}}
const anchor = registry.anchor;
return {{
stats: registry.stats,
dedupe_keys: registry.event_index.dedupe_keys,
activity: anchor.activity_events.map((event) => ({{
event_id: event.event_id,
kind: event.kind,
status: event.status,
text: event.payload && event.payload.text || null,
tool_call_id: event.payload && event.payload.tool_call_id || null,
}})),
terminal_state: anchor.lifecycle.terminal_state,
final_answer: anchor.content.final_answer,
final_message_ref: anchor.content.final_message_ref,
usage: anchor.usage,
}};
}}
console.log(JSON.stringify({{
replayFirst: build('replay-first'),
settledFirst: build('settled-first'),
}}));
"""
result = subprocess.run([NODE, "-e", script], text=True, capture_output=True, check=False)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_registry_owns_one_anchor_and_dedupes_live_plus_replay_events():
data = _registry_snapshot()
registry = data["registry"]
anchor = registry["anchor"]
assert data["version"] == "slice3-registry-shadow"
assert [item["reason"] for item in data["results"][:2]] == [None, "duplicate"]
assert registry["event_index"]["dedupe_keys"][:2] == [
'event_id:"run-1:1"',
'event_id:"run-1:2"',
]
assert registry["stats"]["applied"] == 7
assert registry["stats"]["skipped_duplicate"] == 1
assert registry["stats"]["skipped_mismatched"] == 1
assert anchor["identity"]["session_id"] == "sid-1"
assert anchor["identity"]["turn_id"] == "turn-1"
assert anchor["identity"]["run_id"] == "run-1"
assert anchor["identity"]["stream_id"] == "stream-1"
assert [event["kind"] for event in anchor["activity_events"]] == [
"process_prose",
"reasoning",
"terminal_status",
]
assert anchor["activity_events"][0]["payload"] == {"text": "live token"}
def test_registry_routes_activity_artifacts_side_effects_metadata_and_transport():
data = _registry_snapshot()
anchor = data["registry"]["anchor"]
assert len(anchor["artifacts"]) == 1
assert anchor["artifacts"][0]["source_event_type"] == "artifact_reference"
assert anchor["artifacts"][0]["payload"] == {
"kind": "workspace_file",
"path": "answer.txt",
}
assert len(anchor["side_effects"]) == 1
assert anchor["side_effects"][0]["source_event_type"] == "state_saved"
assert len(anchor["metadata_events"]) == 1
assert anchor["metadata_events"][0]["source_event_type"] == "settled_message"
assert len(anchor["transport_events"]) == 1
assert anchor["transport_events"][0]["source_event_type"] == "stream_end"
def test_registry_updates_lifecycle_and_settled_final_projection():
data = _registry_snapshot()
anchor = data["registry"]["anchor"]
assert anchor["lifecycle"]["status"] == "completed"
assert anchor["lifecycle"]["terminal_state"] == "completed"
assert anchor["lifecycle"]["started_at"] == "2026-06-11T00:00:01Z"
assert anchor["lifecycle"]["completed_at"] == "2026-06-11T00:00:06Z"
assert anchor["content"]["final_answer"] == "final answer"
assert anchor["content"]["final_message_ref"] == "message-final"
assert anchor["usage"] == {"input_tokens": 8, "output_tokens": 13}
def test_registry_replay_and_settlement_order_converge_on_same_anchor_state():
data = _race_snapshot()
replay_first = data["replayFirst"]
settled_first = data["settledFirst"]
assert replay_first == settled_first
assert replay_first["stats"]["applied"] == 4
assert replay_first["stats"]["skipped_duplicate"] == 1
assert replay_first["dedupe_keys"] == [
'event_id:"run-race:1"',
'event_id:"run-race:2"',
'event_id:"run-race:3"',
]
assert replay_first["activity"] == [
{
"event_id": "run-race:1",
"kind": "process_prose",
"status": None,
"text": "live token",
"tool_call_id": None,
},
{
"event_id": "run-race:2",
"kind": "tool_completed",
"status": "completed",
"text": None,
"tool_call_id": "tool-1",
},
{
"event_id": "run-race:3",
"kind": "terminal_status",
"status": "completed",
"text": None,
"tool_call_id": None,
},
]
assert replay_first["terminal_state"] == "completed"
assert replay_first["final_answer"] == "race final"
assert replay_first["final_message_ref"] == "message-race"
assert replay_first["usage"] == {"input_tokens": 5, "output_tokens": 8}
def test_registry_does_not_destructively_dedupe_seqless_local_tool_lifecycle():
data = _hardening_snapshot()
registry = data["toolRegistry"]
anchor = registry["anchor"]
assert data["version"] == "slice3-registry-shadow"
assert data["toolResults"] == [
{"applied": True, "reason": None},
{"applied": True, "reason": None},
{"applied": True, "reason": None},
{"applied": True, "reason": None},
{"applied": True, "reason": None},
]
assert registry["event_index"]["dedupe_keys"] == []
assert registry["stats"]["applied"] == 5
assert [event["kind"] for event in anchor["activity_events"]] == [
"tool_started",
"tool_updated",
"tool_completed",
"process_prose",
"process_prose",
]
def test_registry_rejects_cross_run_events_but_allows_stream_reconnects():
data = _hardening_snapshot()
registry = data["runRegistry"]
assert data["runMismatch"] == {"applied": False, "reason": "mismatched_anchor"}
assert data["streamAccepted"] == {"applied": True, "reason": None}
assert registry["stats"]["skipped_mismatched"] == 1
assert registry["stats"]["applied"] == 1
assert registry["anchor"]["identity"]["run_id"] == "run-a"
assert registry["anchor"]["identity"]["stream_id"] == "stream-a"
assert registry["anchor"]["activity_events"][0]["stream_id"] == "stream-b"
def test_registry_identity_copy_and_metadata_reads_are_hardened():
data = _hardening_snapshot()
identity_registry = data["identityRegistry"]
metadata_anchor = data["metadataRegistry"]["anchor"]
assert identity_registry["identity"]["session_id"] == "sid-freeze"
assert identity_registry["anchor"]["identity"]["session_id"] == "sid-freeze"
assert metadata_anchor["content"]["final_answer"] == "structured answer"
assert metadata_anchor["content"]["final_message_ref"] == "message-structured"
assert metadata_anchor["usage"] == {"input_tokens": 8, "output_tokens": 13}
assert len(metadata_anchor["metadata_events"]) == 2
def test_shadow_snapshot_feeds_current_source_families_into_one_registry_owner():
data = _shadow_snapshot()
registry = data["registry"]
anchor = registry["anchor"]
assert data["version"] == "slice3-registry-shadow"
assert data["results"]["live"] == [{"applied": True, "reason": None}]
assert data["results"]["replay"] == [
{"applied": False, "reason": "duplicate"},
{"applied": True, "reason": None},
]
assert data["results"]["settled"] == [{"applied": True, "reason": None}]
assert data["results"]["inflight"] == [{"applied": True, "reason": None}]
assert registry["stats"]["applied"] == 4
assert registry["stats"]["skipped_duplicate"] == 1
assert anchor["identity"]["run_id"] == "run-shadow"
assert anchor["identity"]["stream_id"] == "stream-shadow"
assert [event["kind"] for event in anchor["activity_events"]] == [
"process_prose",
"tool_completed",
]
assert [event["source_event_type"] for event in anchor["metadata_events"]] == [
"settled_message",
"inflight_snapshot",
]
assert anchor["content"]["final_answer"] == "shadow final"
def test_registry_instances_do_not_share_owner_state():
data = _registry_snapshot()
isolated = data["isolated"]
assert isolated["identity"]["turn_id"] == "turn-2"
assert isolated["event_index"]["dedupe_keys"] == []
assert isolated["stats"]["applied"] == 0
assert isolated["anchor"]["activity_events"] == []
def test_slice3_registry_is_still_unwired_from_rendering_hot_paths():
helper_names = [
"createAssistantTurnAnchorRegistry",
"applyAssistantTurnAnchorNormalizedEvent",
"applyAssistantTurnAnchorSourceEvent",
"applyAssistantTurnAnchorSourceEvents",
"createAssistantTurnAnchorShadowSnapshot",
]
for helper in helper_names:
assert helper not in _read(UI_JS)
assert helper not in _read(SESSIONS_JS)
assert helper not in _read(MESSAGES_JS)