diff --git a/static/messages.js b/static/messages.js index efae04f61..cd3ba6de2 100644 --- a/static/messages.js +++ b/static/messages.js @@ -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}); diff --git a/static/ui.js b/static/ui.js index b34c0f430..c443f0351 100644 --- a/static/ui.js +++ b/static/ui.js @@ -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; } diff --git a/tests/test_issue5700_worklog_event_timestamps.py b/tests/test_issue5700_worklog_event_timestamps.py index 9c425da64..4ef424c8c 100644 --- a/tests/test_issue5700_worklog_event_timestamps.py +++ b/tests/test_issue5700_worklog_event_timestamps.py @@ -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( """