Reducing memory should not begin by closing a tab where someone is writing. Keeping every reading result forever creates another kind of waste. I turned the browser's tab-protection and reading-cache boundaries into two synthetic policies that can be inspected without touching a real browser.
Separate a candidate decision from actually closing a tab
Sleep_decision evaluates a supplied TabState and returns eligibility with blocking reasons. Current/reference/pinned tabs, private or agent-owned work, loading, transfer, playback, capture, developer tools, interaction and drafts are protection conditions. HTTPS reading, all-frame safety and unload permission must also be explicit.
Those values are caller declarations. The example inspects no frame and executes no beforeunload handler. Even a True candidate leaves renderer_released and live_properties_verified False. The original host performs asynchronous frame checks, rechecks context and attempts a real close. This smaller policy function does not borrow those operational responsibilities or their evidence.
Evaluate time only when the caller supplies it
The default idle threshold is five minutes. Future last-activity time is rejected, and a protection delay prevents candidacy until its boundary. Strings cannot masquerade as boolean safety checks. The example deliberately avoids convenient truthiness that would turn missing safety evidence into permission.
There is no real clock or periodic task. Without another call, changed state is not reevaluated. Cache time likewise uses nonnegative integer timestamps and rejects backward movement. Operational integration needs a trusted clock, current declarations and a recheck between eligibility and action. Those connections are outside this synchronous teaching example.
Cache only title and text without action authority
PassiveCache accepts title and text strings only, deliberately narrower than the original PageView. Extra references and nested objects are rejected rather than becoming future interaction data. Detached input and output prevent a caller mutation from changing retained reading content.
Context contains profile, project, tab, document, epoch and revision. A changed document, epoch or revision for the same tab removes the older item and returns a miss. These are still declarations rather than a live authenticated site revision. Action_authority and live_revision_authenticated always stay False, so a cache hit cannot create authority for a fresh click.
Explore the implementation
Complete independent synthetic implementation
"""Independent declared-state policy. No renderer, browser, filesystem or timer."""
from dataclasses import dataclass, fields
from copy import deepcopy
import json
def integer(value, minimum=0):
if type(value) is not int or value < minimum:
raise ValueError('INTEGER_REQUIRED')
return value
@dataclass(frozen=True)
class TabState:
last_active: int
blocked_until: int = 0
active: bool = False
reference: bool = False
pinned: bool = False
incognito: bool = False
agent_owned: bool = False
loading: bool = False
transfer: bool = False
audible: bool = False
captured: bool = False
devtools: bool = False
navigation_pending: bool = False
interacted: bool = False
draft: bool = False
auth: bool = False
https_read: bool = False
all_frames_safe: bool = False
unload_allowed: bool = False
dormant: bool = False
releasing: bool = False
def __post_init__(self):
integer(self.last_active);integer(self.blocked_until)
for field in fields(self):
if field.name not in ('last_active','blocked_until') and type(getattr(self,field.name)) is not bool:
raise ValueError('EXPLICIT_BOOLEAN_REQUIRED')
def sleep_decision(state, now, idle_ms=300000):
if type(state) is not TabState:
raise ValueError('TAB_STATE_REQUIRED')
integer(now);integer(idle_ms,1)
if now < state.last_active:
raise ValueError('FUTURE_ACTIVITY')
blocked=[f.name for f in fields(state) if f.name not in ('last_active','blocked_until','https_read','all_frames_safe','unload_allowed') and getattr(state,f.name)]
blocked += [key for key in ('https_read','all_frames_safe','unload_allowed') if not getattr(state,key)]
if now-state.last_active < idle_ms:blocked.append('not_idle')
if now < state.blocked_until:blocked.append('blocked_until')
return {'candidate':not blocked,'blocked_by':blocked,'live_properties_verified':False,'renderer_released':False}
@dataclass(frozen=True)
class Context:
profile: str
project: str
tab: str
document: str
epoch: int
revision: str
def __post_init__(self):
for key in ('profile','project','tab','document','revision'):
v=getattr(self,key)
if type(v) is not str or not v.strip() or len(v)>200:
raise ValueError('CONTEXT_STRING_REQUIRED')
integer(self.epoch,1)
class PassiveCache:
def __init__(self,max_entries=4,max_bytes=4194304,max_entry_bytes=1048576,ttl_ms=300000):
for v in (max_entries,max_bytes,max_entry_bytes,ttl_ms):integer(v,1)
self.limits=(max_entries,max_bytes,max_entry_bytes,ttl_ms)
self._entries={};self._clock=-1;self._sequence=0
def _time(self,now):
integer(now)
if now<self._clock:raise ValueError('CLOCK_MOVED_BACKWARDS')
self._clock=now
@staticmethod
def _key(context):
if type(context) is not Context:raise ValueError('CONTEXT_REQUIRED')
return (context.profile,context.project,context.tab)
def _prune(self,now):
for k,e in list(self._entries.items()):
if now-e['touched']>=self.limits[3]:del self._entries[k]
while len(self._entries)>self.limits[0] or sum(e['bytes'] for e in self._entries.values())>self.limits[1]:
k=min(self._entries,key=lambda k:(self._entries[k]['touched'],self._entries[k]['sequence']))
del self._entries[k]
def put(self,context,view,now):
key=self._key(context)
if type(view) is not dict or set(view)!={'title','text'} or any(type(v) is not str for v in view.values()):
raise ValueError('PASSIVE_TITLE_TEXT_ONLY')
b=len(json.dumps(view,ensure_ascii=False,separators=(',',':')).encode('utf-8'))
self._time(now);self._prune(now)
if b>min(self.limits[1],self.limits[2]):
self._entries.pop(key,None)
return {'cached':False,'reason':'oversized','bytes':b,'action_authority':False}
self._sequence+=1
self._entries[key]={'context':context,'view':deepcopy(view),'bytes':b,'touched':now,'sequence':self._sequence}
self._prune(now)
return {'cached':key in self._entries,'bytes':b,'action_authority':False}
def get(self,context,now):
key=self._key(context);self._time(now);self._prune(now)
entry=self._entries.get(key)
if entry is None:return None
if entry['context']!=context:
del self._entries[key];return None
entry['touched']=now
return {'view':deepcopy(entry['view']),'action_authority':False,'live_revision_authenticated':False}
def stats(self,now):
self._time(now);self._prune(now)
return {'entries':len(self._entries),'bytes':sum(e['bytes'] for e in self._entries.values()),'renderer_released':False}
Bound count, total bytes, entry size and idle age separately
Defaults are four entries, four MiB total, one MiB per entry and five minutes idle age. Size means compact uncompressed UTF-8 JSON bytes in this example, not total Python heap or Electron RAM. A test distinguishes Korean and ASCII byte counts at equal character length.
Reads refresh touched time; pressure removes least recently touched entries, with insertion order breaking timestamp ties. Age at or beyond TTL expires an entry. An oversized replacement also removes the old same-tab value instead of quietly returning stale content. A miss therefore has a clearer meaning than retaining earlier data after a rejected new observation.
Thirty tests cover both protection and eviction
Tests cover current/reference/working tabs, unverified frames, unload refusal and the delay boundary. Cache cases distinguish out-of-scope misses, document/epoch/revision changes, TTL boundaries, age refresh on hits, LRU, total bytes and oversized entries. Mutating either an input or a returned copy must not affect the retained source.
Thirty tests run against the independent source and again after ZIP extraction. Five records come from actual local function calls; the selector reads those results. This is not a renderer-release or real-account memory measurement. Original product test totals are not added to the new example's execution count.
Leave operational integration responsibilities explicit
The example implements no asynchronous read coalescing, generation fencing during observation, driver frame-map release, fallback page reading or automatic UI message. The original product connects those responsibilities separately. Fewer retained cache bytes do not establish an equal reduction in real process RAM.
Code, tests, record generation and documentation permit modification and reuse. Adapting protection conditions for another site must preserve checks that protect current human work first. I share separate starting points for deciding what may sleep and what reading data should remain, rather than hiding both decisions behind one memory-saving button.