mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-18 21:50:55 +00:00
fix(worklog): persist event timestamps through settlement + reject invalid epochs
Two gate findings on the worklog-event-timestamp feature (#5700/#5739): 1. Thinking-row timestamps disappeared after settlement. Reasoning source events carry no server timestamp; the live DOM showed a fallback time but the settled scene row rebuilt with created_at:null. Now stamp created_at on the source event the first time it's seen (messages.js apply path) and carry row.created_at through snapshot hydration, so the timestamp survives live->settled. 2. A garbage huge numeric timestamp (e.g. 1e20) passed the finite/>0 checks and rendered literal 'Invalid Date'. _timestampSeconds now rejects epochs outside JavaScript's valid Date range. Added a regression test. Co-authored-by: rodboev. Gate findings from adversarial Codex re-gate.
This commit is contained in:
@@ -2601,6 +2601,13 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
const sourceEvent={
|
||||
...raw,
|
||||
source_event_type:sourceEventType,
|
||||
// Persist a creation timestamp the FIRST time we see this source event, so
|
||||
// the worklog event timestamp (#5700/#5739) survives settlement. Reasoning
|
||||
// events carry no server timestamp; without this, the live DOM shows a
|
||||
// fallback time but the settled scene row rebuilds with created_at:null and
|
||||
// the timestamp disappears. Prefer any real server-supplied stamp; fall back
|
||||
// to now only when none exists. (#5739 gate finding.)
|
||||
created_at:raw.created_at??raw.timestamp??raw.ts??(Date.now()/1000),
|
||||
activitySegmentSeq:raw.activitySegmentSeq??raw.activity_segment_seq??_assistantSegmentSeq,
|
||||
activityBurstId:raw.activityBurstId??raw.activity_burst_id??_currentActivityBurstId,
|
||||
};
|
||||
@@ -3890,6 +3897,10 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
status:row.status||undefined,
|
||||
stream_id:row.stream_id||streamId,
|
||||
run_id:row.run_id||streamId,
|
||||
// Carry the row's persisted creation timestamp through hydration so the
|
||||
// worklog event timestamp (#5700/#5739) survives a settled-snapshot rebuild
|
||||
// (payload may not carry created_at even when the row does). (#5739 gate.)
|
||||
created_at:payload.created_at??row.created_at??undefined,
|
||||
};
|
||||
try{
|
||||
_anchorApi.applyAssistantTurnAnchorSourceEvent(_anchorRegistry,sourceEvent,{session_id:activeSid,stream_id:streamId,run_id:streamId});
|
||||
|
||||
+9
-3
@@ -5572,18 +5572,24 @@ function _timestampSeconds(value){
|
||||
if(value===undefined||value===null||value==='') return null;
|
||||
if(value instanceof Date){
|
||||
const stamp=value.getTime()/1000;
|
||||
return Number.isFinite(stamp)&&stamp>0?stamp:null;
|
||||
return (Number.isFinite(stamp)&&stamp>0&&Math.abs(stamp)<=8.64e12)?stamp:null;
|
||||
}
|
||||
const numeric=Number(value);
|
||||
if(Number.isFinite(numeric)&&numeric>0){
|
||||
const stamp=numeric>1e12?numeric/1000:numeric;
|
||||
return Number.isFinite(stamp)&&stamp>0?stamp:null;
|
||||
// Reject epochs outside JavaScript's valid Date range (±8.64e15 ms = ±8.64e12 s);
|
||||
// otherwise new Date(stamp*1000) yields "Invalid Date" and renders literally
|
||||
// (e.g. a garbage numeric timestamp like 1e20 passes finite/>0). (#5739 gate.)
|
||||
return (Number.isFinite(stamp)&&stamp>0&&stamp<=8.64e12)?stamp:null;
|
||||
}
|
||||
if(typeof value==='string'){
|
||||
const text=value.trim();
|
||||
if(!text||/^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(text)) return null;
|
||||
const parsed=Date.parse(text);
|
||||
if(Number.isFinite(parsed)&&parsed>0) return parsed/1000;
|
||||
if(Number.isFinite(parsed)&&parsed>0){
|
||||
const stamp=parsed/1000;
|
||||
return stamp<=8.64e12?stamp:null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -427,6 +427,37 @@ process.stdout.write(JSON.stringify({
|
||||
assert data["nil"] is None
|
||||
|
||||
|
||||
def test_timestamp_seconds_rejects_out_of_range_epochs_so_invalid_date_never_renders():
|
||||
# A garbage huge numeric timestamp (e.g. 1e20) passes the finite/>0 checks but
|
||||
# is far past JavaScript's max representable Date (±8.64e15 ms), so without a
|
||||
# range guard new Date(stamp*1000) yields "Invalid Date" and would render
|
||||
# literally in the worklog time label. (#5739 gate finding.)
|
||||
data = _run_node(
|
||||
"""
|
||||
process.stdout.write(JSON.stringify({
|
||||
huge: _timestampSeconds(1e20),
|
||||
hugeMs: _timestampSeconds(1e20 * 1000),
|
||||
normal: _timestampSeconds(1700000901),
|
||||
normalMs: _timestampSeconds(1700000901000),
|
||||
hugeLabel: _activityClockLabel(_timestampSeconds(1e20) || undefined),
|
||||
normalLabel: _activityClockLabel(_timestampSeconds(1700000901)),
|
||||
}));
|
||||
"""
|
||||
)
|
||||
|
||||
# garbage huge epochs (whether given as seconds or ms) resolve to null
|
||||
assert data["huge"] is None
|
||||
assert data["hugeMs"] is None
|
||||
# a normal recent epoch still resolves, in both seconds and ms forms
|
||||
assert data["normal"] == 1700000901
|
||||
assert data["normalMs"] == 1700000901
|
||||
# the clock label for a rejected timestamp must never be the literal "Invalid Date";
|
||||
# a valid one produces a real time string.
|
||||
assert "Invalid Date" not in (data["hugeLabel"] or "")
|
||||
assert "Invalid Date" not in (data["normalLabel"] or "")
|
||||
assert data["normalLabel"]
|
||||
|
||||
|
||||
def test_settled_anchor_scene_rows_use_row_created_at_timestamps():
|
||||
data = _run_node(
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user