Dom Sentinel · eyes
A browser-side engine that reads the live web and heals itself when it changes. You describe the elements you care about by intent, each with an ordered chain of fallback selectors. Sentinel watches the page; when a site ships new markup and your selectors break, it detects that, proposes replacements, and you swap them in — without shipping a new build.
| Property | Value |
|---|---|
| Shape | Browser JavaScript library, ES modules |
| Runtime dependencies | None required. Vision providers are optional peers. |
| Backend | None — it makes no network calls of its own |
| Types | JSDoc-typed source; ambient declarations maintained by the consumer |
Getting started
import { DOMSentinel } from '@sekondbrainailabs/kora-sentinel';
import { GroqProvider } from '@sekondbrainailabs/kora-sentinel/providers/groq';
const sentinel = new DOMSentinel({
appId: 'my-app',
elements: {
postContainer: {
selectors: ['[role="listitem"]', 'article', '.feed-item'], // ordered fallback chain
description: 'A feed post container with title and author',
required: true,
expectedCount: { min: 1, max: 50 },
},
},
visionAdapter: new GroqProvider({ apiKey: '<YOUR_PROVIDER_KEY>' }),
onResetDetected: async (result) => {
if (result.reason !== 'dom_reset') return;
const { proposedSelectors } = await sentinel.analyze();
if (proposedSelectors.postContainer) {
sentinel.updateSelectors('postContainer', proposedSelectors.postContainer);
}
},
});
sentinel.start();
sentinel.watch('postContainer', (els) => console.log(`${els.length} posts`));
Constructor options
| Option | Default | Meaning |
|---|---|---|
appId | — | Required. Namespaces stored state. |
elements | — | Required. Your element definitions. |
visionAdapter | null provider | Optional. Supplies selector proposals when healing. |
screenshotFn | — | Async function returning a base64 screenshot. |
onResetDetected | — | Fires when the page structure breaks. Branch on result.reason. |
onSelectorsUpdated | — | Fires when a selector chain is replaced — persist it here. |
resetThresholds | see below | Cooldowns and sensitivity. |
observerDebounceMs | 500 | Mutation batching window. |
Methods
Attach and detach the observer. Attributes are deliberately not observed — modern frameworks rewrite data- and aria- on every render, and watching them is pure noise.
Walks the selector chain and returns the first non-empty result. Invalid selectors are skipped rather than thrown.
Pulls named fields out of a matched element using its extract specs.
Per-element found / count / matched-selector / in-expected-range, and a document-level healthy flag that is false when any required element matched nothing.
Subscribe to a debounced batch. Callback errors are caught, so one bad handler can't stop the observer.
Health check, then a capture, then ask the vision provider for replacement selectors. Returns healthBefore, proposedSelectors and visionAvailable. Provider failures are caught, not thrown.
Swap a chain at runtime. Both fire onSelectorsUpdated so you can persist the repair.
Add elements after construction, or reach the registry directly.
Element definitions
An element is described by what it is, not only by how to find it — the description is what a model reads when proposing a repair.
{
selectors: ['[data-testid="msg"]', 'article', '.message'], // required, ordered
description: 'A chat message bubble', // required — read by the healer
required: true, // zero matches ⇒ structural break
expectedCount: { min: 1, max: 200 },
scope: 'document', // or an Element
fields: {
author: { selectors: ['.author'], extract: 'text', description: 'Sender name' },
href: { selectors: ['a'], extract: 'attr:href | text', description: 'Permalink' },
posted: { selectors: ['time'], extract: 'attr:datetime',
validator: '^\\d{4}-\\d{2}-\\d{2}', description: 'ISO timestamp' }
}
}
| Extract spec | Yields |
|---|---|
text | Trimmed text content. |
attr:NAME | That attribute, trimmed. |
regex:PATTERN | First capture group, matched case-insensitively. |
A | B | C | Fallback chain — first non-empty wins. |
A validator regex discards values that don't match, so a mis-healed selector yields nothing rather than garbage. Definitions are validated on registration with precise errors — a missing description or an expectedCount whose min exceeds its max throws immediately rather than failing mysteriously later.
Self-healing
Two independent mechanisms — use either, or both.
Detection
Every debounced mutation batch runs a check. Results carry a reason:
| Reason | Fires when |
|---|---|
dom_reset | A required element matched nothing — the structural break you care about. |
quality_drop | Extractions are matching but coming back empty or too short. |
daily_check | A scheduled once-a-day nudge. Fires even on a healthy page — always branch on reason. |
Detections are rate-limited by cooldown — a six-hour window for structural breaks, an hour for quality drops — so a thrashing page can't trigger a repair storm. State persists in extension storage where available, memory otherwise.
Repair, without a model
The interesting path needs no LLM at all. Fingerprint an element while it works, and later find it again by similarity:
A fingerprint captures role, ARIA, heading presence, depth, child count, text shape and neighbouring text. Candidates are scored against it, a stable selector is synthesised for the winner, and an oracle decides whether to accept — rejecting on zero-match, count-explosion, includes-other-role or low-similarity.
Selector synthesis prefers what survives a redesign: roles and ARIA first, then stable data attributes, then ids and classes — explicitly skipping framework-generated hashes and build-time class noise. A proposal is only accepted if it matches tightly, and healElement returns a proposal without mutating your registry, so you decide whether to trust it.
An optional model tiebreak was measured and removed — pure structural similarity scored better. The model path is for proposing selectors from a description; the fingerprint path is for re-finding a known element, and it is faster, free and offline.
Vision providers
Bring your own. A provider implements three calls — propose selectors from a DOM chunk, count elements visually, and verify an injection. Groq and OpenAI providers ship; the interface is small enough to implement against any model.
Media capture
Materialises images before their object URLs expire, scaling the longest edge down and encoding to base64. Cross-origin images are skipped rather than silently producing a tainted canvas, and avatars, logos and tracking pixels are filtered out by default.
Intercepts a user's file selection before the page uploads it, returning name, MIME type, size and contents. It attaches both directly and via capture-phase delegation, so inputs a framework creates later are still caught. Oversized files are reported with metadata and no payload.
When Sentinel serialises a page for a vision provider, it detects conversational content — message and response containers, article and log roles, long leaf text — and replaces it with a redaction marker. The provider receives structure, not what was said. Remaining text is truncated. This is on by default, not a setting you have to find.
Browser Control · hands
Reading a page is half of it. Browser Control drives one — opening and navigating conversations, scrolling long or virtualised feeds until they settle, and clicking through multi-step task interfaces — so an agent can operate a web app rather than only observe it. It runs on the same self-healing selector engine, so actions survive a redesign.
| Option | Default | Meaning |
|---|---|---|
onProgress | — | Progress callback for long crawls. |
maxConversations | unbounded | Cap the run. |
delayBetweenMs | 1500 | Human-paced gap between navigations. |
navigationTimeoutMs | 12000 | Per-navigation ceiling. |
sidebarScrollPasses | 5 | Minimum passes before convergence checks. |
Scroll until settled, not scroll N times
Infinite feeds don't finish — they converge. Control scrolls in bounded passes and stops when the content stops growing across several consecutive passes, rather than guessing a fixed count. It finds the real scroll container instead of scrolling the window, waits for the message count to hold steady before extracting, and recognises "load more" affordances. Every loop carries a hard pass and time ceiling, so a pathological page ends the crawl instead of hanging it.
Supported surfaces
Each surface has an adapter supplying its navigation, conversation list and message extraction, behind one interface. Where a platform offers no navigable links, Control enumerates by clicking through the list instead — with stall detection so it stops rather than spinning.
Capture is resilient by design: consecutive extraction failures trip a threshold that marks the session degraded and triggers a heal attempt, rather than quietly returning empty results.
Control acts on pages in the user's own signed-in browser, on sites they have enabled, at human pace, with a kill-switch. Automating a third-party interface can conflict with that platform's terms — that judgement stays with you, and the pacing and fallback behaviour exist to keep it conservative.
Kora Voice · voice
A complete spoken-conversation layer for React: speech-to-text, text-to-speech, voice-activity detection, break detection, a conversation state machine and a waveform UI. You supply one function — how to answer a message — and it handles the rest.
import { VoiceConversationProvider, VoiceConversationUI } from '@kora/voice-conversation';
<VoiceConversationProvider
config={{
breakDetection: { enabled: true, silenceThreshold: 1500,
volumeThreshold: 20, minSpeechDuration: 500,
continuationWindow: 2000 },
autoSend: { enabled: true, confirmationRequired: false, showTranscript: true },
providers: {
stt: { provider: 'openai', apiKey: '<YOUR_KEY>', model: 'whisper-1' },
tts: { provider: 'openai', apiKey: '<YOUR_KEY>', voice: 'alloy' },
},
}}
onSendMessage={async (text, history) => (await myBackend(text, history)).reply}
>
<VoiceConversationUI />
</VoiceConversationProvider>
Providers are pluggable: browser-native speech (no key), OpenAI, or Groq for transcription; browser or OpenAI for speech. A custom provider is two methods.
Voice configuration
Break detection — knowing when you've finished
The hard part of voice isn't transcription, it's deciding when a person has actually stopped talking rather than drawn breath. Break detection is a pure, testable function over volume and elapsed time:
| Setting | Default | Effect |
|---|---|---|
silenceThreshold | 1500 ms | Silence after speech that counts as a finished utterance. |
volumeThreshold | 20 | Level above which audio counts as speech. |
minSpeechDuration | 500 ms | Ignore blips shorter than this. |
continuationWindow | 2000 ms | Resume within this and it's the same thought; after it, a new utterance. |
That continuation window is why a pause mid-sentence doesn't fire the message off early — the outcomes are explicit: speaking, silence, continuation, new_utterance or break.
Other settings
| Group | Controls |
|---|---|
autoSend | Send on break, require confirmation, show the transcript. |
conversation | Auto-play replies, allow interruption, keep context, history length (default 50). |
ui | Waveform, transcript, controls, light/dark/auto. |
callbacks | State changes, transcripts, messages, errors, break and continuation. |
Pass providers and ui complete — they replace defaults rather than merging.
Hooks & state
The provider component is a convenience. Every layer is available on its own:
| Hook | Gives you |
|---|---|
useVoiceConversation | The whole orchestrator — state, transcripts, messages, volume, and startListening, sendMessage, interrupt, pause, resume, reset. |
useVoiceInput | Recording, live and accumulated transcript, silence and speech duration. |
useVoiceOutput | Playback with speak, stop, pause, resume — plus a spoken-character index for karaoke-style highlighting. |
useVoiceActivityDetection | Raw isSpeaking, volume, silence and speech duration. |
detectBreak | The pure break-detection function, unit-testable in isolation. |
The state machine is explicit
idle → listening → processing → sending → thinking → speaking → idle, with waiting_send when you require confirmation and error as a dead end that only reset leaves. A continuation returns listening to itself; an interrupt during thinking or speaking goes straight back to listening. Transitions are exported and validatable, so illegal states are unreachable rather than merely unlikely.
Transcription results carry more than text where the provider supports it — timed segments, per-word timings and speaker labels — which is what makes the diarisation below possible.
Diarisation · ears
Voice tells you what was said. Diarisation tells you who said it. Enrol a speaker once, then identify them in real time in a room of several people — with per-person context and permissions.
Client
A useSpeakerIdentification hook wraps this with attempt limits and caching — once identified it stops asking — and a drop-in VoiceEnrollment component handles the capture flow, requesting a 16 kHz mono stream with echo cancellation and noise suppression.
Identification is two-stage, and says when it isn't sure
A single similarity score is not enough to act on. The engine returns a decision built from several signals:
| Signal | Meaning |
|---|---|
identified | Best match cleared the identification floor. |
verified | Cleared the higher confirmation floor and was unambiguous and in-session. This is the one to gate on. |
ambiguous | Top match didn't beat the runner-up by enough — two similar voices, so no claim is made. |
out_of_session | Someone outside the expected roster matched better than anyone in it — meaning the real speaker isn't enrolled here, so the in-room runner-up must not be trusted. |
all_scores | Every candidate's score, so you can show your own working. |
Enrolment is collision-checked: a new voice too close to a different enrolled user is rejected rather than quietly creating a second identity for the same person.
Beyond identification
| Capability | What it does |
|---|---|
| Diarisation | Segments a recording into who-spoke-when using a sliding window, merging consecutive turns. Unmatched speech is labelled as a guest rather than guessed. |
| Transcript | Diarised segments with spoken text attached, where transcription is available. |
| Floor control | Push-to-talk with a holder and a queue, so several people share one agent without talking over each other. |
| Wake check | Order-independent wake-phrase matching, tolerant of homophones and near-misses. |
Voice signatures are compact embeddings — two engines are selectable, and audio is conditioned first: noise reduction, a speech-band filter, pre-emphasis and level normalisation. Multiple enrolments average into one profile per speaker.
Your data
On the web. Sentinel and Control read and act only on the pages you enable, and only to do the job you asked. During a self-heal, page structure is sent to the vision provider you configure, with conversational content redacted before it leaves. You hold the provider key. The fingerprint-based repair path sends nothing anywhere.
Diarisation creates a voiceprint — a biometric identifier, and special-category personal data in most jurisdictions. Enrolment must be explicit and informed. Raw audio is not retained; profiles are stored as embeddings and are deletable per user. If you select a cloud transcription provider, audio is sent to it — a browser-native provider keeps it on the device. Handling, consent and erasure are set out in the privacy policy.
Availability & access
HEVE is in preview. All four capabilities are built and run inside SeKondBrain today. They ship as part of the platform — licensed, not open source — and the packages are distributed to integration partners rather than published openly, so the APIs above describe what you get on access rather than something you can install unaided.
Where it runs
Sentinel, Browser Control and the Voice library run in your own application — they are client-side, and the pages and audio they touch never pass through us. Only diarisation needs a service behind it, and that follows the platform model:
| Model | What it means |
|---|---|
| Multi-tenant cloud | The default. We operate the identity service; your organisation gets its own isolated tenant and endpoint. |
| Your own cloud | For enterprise. Deployed into your cloud account, so voiceprints stay inside your boundary — usually the deciding factor for biometric data. |
What you get when you're set up
| The packages | Sentinel and the Voice library, distributed to you directly with your licence. |
| A unique URL | For diarisation — your own identity-service endpoint and credentials. |
| Provider configuration | Guidance on wiring your own vision and speech provider keys, which you hold. |
| Setup guide | Provisioning covers integration into your extension or app, and enrolment for diarisation. |
Email hello@sekondbrain.ai with the subject “Access for HEVE” — say which of the four you need: Dom Sentinel, Browser Control, Kora Voice, Diarisation.