@deepwatch/dsh-live 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":["toneFor","_jsxs","_jsx","_Fragment","toneFor"],"sources":["../../brand/lib/identity.js","../../brand/lib/mark.js","../../workspace/lib/client/surface.js","../src/sources-catalogue.ts","../src/client/live-mode.tsx","../src/session.ts","../src/client/components.tsx","../src/client/index.tsx"],"sourcesContent":["/**\n * The Watch product identity, and the one colour rule that matters.\n *\n * Kept as data rather than as markup so the same strings reach the sidebar,\n * the conversation hero, the About panel, the window title and the release\n * notes without four copies drifting apart. Attribution in particular has to\n * be identical everywhere it appears, because it is a legal statement rather\n * than a design element.\n *\n * @module @deepwatch/dsh-client-brand/identity\n */\n/** What the product is called. Never \"DeepSeek\" anything. */\nexport const PRODUCT_NAME = 'DeepWatch';\n/** The short form, for a tab title or a cramped header. */\nexport const PRODUCT_SHORT_NAME = 'Watch';\n/**\n * The document title this product should be showing.\n *\n * The `<title>` belongs to DSH's built HTML shell, which this distribution\n * does not fork, and DSH's session layer rewrites it on every navigation as\n * `<session> — <foundation>`. Left alone that reads\n * `Say hello — DeepSeek Harness · DeepWatch`: two products named in one tab,\n * and the wrong one first.\n *\n * So both names come off before ours goes back on, and ours comes off *first*\n * — the observer that calls this fires on the change this makes, and a version\n * that stripped only the foundation appended a second `· DeepWatch` each time.\n *\n * @param current - the title as it stands right now.\n * @param foundation - the shell's own title, from before hydration.\n */\nexport function productTitle(current, foundation) {\n const ours = ` · ${PRODUCT_NAME}`;\n let text = current;\n while (text.endsWith(ours))\n text = text.slice(0, -ours.length);\n if (foundation !== '') {\n for (const separator of [' — ', ' · ', ' - ', ' | ']) {\n const suffix = `${separator}${foundation}`;\n if (text.endsWith(suffix)) {\n text = text.slice(0, -suffix.length);\n break;\n }\n }\n }\n return text === '' || text === foundation || text === PRODUCT_NAME\n ? PRODUCT_NAME\n : `${text} · ${PRODUCT_NAME}`;\n}\n/**\n * Attribution to the upstream project.\n *\n * Required, and required to be visible. Watch is built on DeepSeek Harness and\n * says so; the alternative — quietly shipping someone else's foundation — is\n * both wrong and against the MIT notice this distribution inherits.\n */\nexport const ATTRIBUTION = 'Built on DeepSeek Harness · Powered by Watch Skill';\n/**\n * The independence disclosure.\n *\n * Equally required, and for the opposite reason: attribution without it could\n * read as endorsement, and no such endorsement exists.\n */\nexport const INDEPENDENCE = 'DeepWatch and Watch Skill are independent projects and are not affiliated with or endorsed by DeepSeek.';\n/** One line for a tooltip or an empty state. */\nexport const TAGLINE = 'An agent that sees, remembers, and can prove what actually happened.';\n/** Every status the product renders, and the tone it is permitted. */\nexport const STATUS_TONE = {\n // Verification — the only place `success` is reachable.\n VERIFIED: 'success',\n FAILED: 'error',\n UNVERIFIED: 'caution',\n INCONCLUSIVE: 'caution',\n STALE: 'caution',\n BLOCKED: 'caution',\n // Agent execution — deliberately never `success`. A completed turn is a\n // statement about the agent, not about the world.\n queued: 'neutral',\n running: 'active',\n completed: 'info',\n failed: 'error',\n cancelled: 'neutral',\n // Evidence health.\n current: 'neutral',\n gap: 'caution',\n expired: 'caution',\n unavailable: 'caution',\n};\n/**\n * The tone one status is allowed.\n *\n * Anything unrecognized is `neutral`, never `success`. A new status added\n * elsewhere and not registered here renders as unremarkable rather than\n * accidentally as a win.\n */\nexport function toneFor(status) {\n return STATUS_TONE[status] ?? 'neutral';\n}\n/** Whether a status may be rendered with the success affordance. */\nexport function isSuccessTone(status) {\n return toneFor(status) === 'success';\n}\n/**\n * The semantic token a tone maps to.\n *\n * Feature packages ask for a tone and get a CSS variable. They never write a\n * hex value, which is what stops the palette from being re-invented slightly\n * differently in every panel — and what makes a theme change one edit.\n */\nexport function tokenFor(tone) {\n return `var(--watch-tone-${tone})`;\n}\n//# sourceMappingURL=identity.js.map","/**\n * The Watch mark, inlined.\n *\n * Generated by `scripts/brand-assets.mjs` from `assets/watch-orca-master.png`.\n * Do not edit by hand: the master is the brand source of truth and this is a\n * mechanical derivation of it at 64px, which covers a 32px slot at 2x.\n *\n * @module @deepwatch/dsh-client-brand/mark\n */\n/** The orca, as a transparent PNG data URI. */\nexport const WATCH_MARK_PNG = 'data:image/png;base64,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';\n/** The master artwork's own blue. Measured from it, never chosen. */\nexport const MASTER_BLUE = '#3160fc';\n//# sourceMappingURL=mark.js.map","import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from \"react/jsx-runtime\";\nimport { WATCH_MARK_PNG } from '@deepwatch/dsh-client-brand';\nconst MODE_KICKER = {\n Watch: 'Trust layer',\n Live: 'Observation',\n Memory: 'Knowledge',\n Library: 'Evidence library',\n Compare: 'Change analysis',\n};\n/** Stable global class names; their rules live with the product theme. */\nconst C = {\n root: 'watch-mode-root',\n hero: 'watch-mode-hero',\n markFrame: 'watch-mode-mark-frame',\n mark: 'watch-mode-mark',\n heroCopy: 'watch-mode-hero-copy',\n eyebrow: 'watch-mode-eyebrow',\n title: 'watch-mode-title',\n lead: 'watch-mode-lead',\n localBadge: 'watch-mode-local-badge',\n body: 'watch-mode-body',\n empty: 'watch-empty',\n emptyCopy: 'watch-empty-copy',\n sectionLabel: 'watch-section-label',\n emptyShows: 'watch-empty-shows',\n emptyWhy: 'watch-empty-why',\n nextBlock: 'watch-next-block',\n nextList: 'watch-next-list',\n panel: 'watch-panel',\n panelHeading: 'watch-panel-heading',\n facts: 'watch-facts',\n factRow: 'watch-fact-row',\n factKey: 'watch-fact-key',\n factValue: 'watch-fact-value',\n note: 'watch-note',\n noteMark: 'watch-note-mark',\n unavailable: 'watch-unavailable',\n unavailableHead: 'watch-unavailable-head',\n unavailableBadge: 'watch-unavailable-badge',\n unavailableBecause: 'watch-unavailable-because',\n requirements: 'watch-requirements',\n};\n/** The frame: a title, one sentence of what this is, then the body. */\nexport function ModeSurface({ title, lead, children }) {\n return (_jsxs(\"div\", { className: C.root, \"data-watch-mode\": title.toLowerCase(), children: [_jsxs(\"header\", { className: C.hero, children: [_jsx(\"span\", { className: C.markFrame, \"aria-hidden\": \"true\", children: _jsx(\"img\", { className: C.mark, src: WATCH_MARK_PNG, alt: \"\" }) }), _jsxs(\"div\", { className: C.heroCopy, children: [_jsx(\"span\", { className: C.eyebrow, children: `DEEPWATCH / ${MODE_KICKER[title] ?? 'Evidence workspace'}` }), _jsx(\"h2\", { className: C.title, children: title }), _jsx(\"p\", { className: C.lead, children: lead })] }), _jsx(\"span\", { className: C.localBadge, children: \"Local-first\" })] }), _jsx(\"div\", { className: C.body, children: children })] }));\n}\n/**\n * What this surface would show, why it shows nothing, and what to do.\n *\n * All three parts are required. \"No data\" on its own teaches people the product\n * is broken; naming the reason and the next action is the difference between an\n * empty state and a dead end.\n */\nexport function EmptyState({ shows, why, next }) {\n return (_jsxs(\"div\", { className: C.empty, \"data-watch-empty-state\": \"\", children: [_jsxs(\"div\", { className: C.emptyCopy, children: [_jsx(\"span\", { className: C.sectionLabel, children: \"What appears here\" }), _jsx(\"p\", { className: C.emptyShows, children: shows }), _jsx(\"p\", { className: C.emptyWhy, children: why })] }), next.length === 0\n ? null\n : (_jsxs(\"div\", { className: C.nextBlock, children: [_jsx(\"span\", { className: C.sectionLabel, children: \"Start here\" }), _jsx(\"ol\", { className: C.nextList, children: next.map(step => _jsx(\"li\", { children: step }, step)) })] }))] }));\n}\n/** A titled block of content. */\nexport function Panel({ heading, children }) {\n return (_jsxs(\"section\", { className: C.panel, \"data-watch-panel\": \"\", children: [heading === undefined ? null : _jsx(\"h3\", { className: C.panelHeading, children: heading }), children] }));\n}\n/** A key/value grid. Long values wrap rather than forcing the page sideways. */\nexport function Facts({ rows }) {\n return (_jsx(\"dl\", { className: C.facts, children: rows.map(([label, value]) => (_jsxs(\"div\", { className: C.factRow, children: [_jsx(\"dt\", { className: C.factKey, children: label }), _jsx(\"dd\", { className: C.factValue, children: value })] }, label))) }));\n}\n/** A short aside in the product's own voice, marked by the accent rule. */\nexport function Note({ children }) {\n return (_jsxs(\"aside\", { className: C.note, children: [_jsx(\"span\", { className: C.noteMark, \"aria-hidden\": \"true\", children: \"i\" }), _jsx(\"p\", { children: children })] }));\n}\n/**\n * A capability this build cannot reach, said out loud.\n *\n * Distinct from `EmptyState`: empty means nothing has happened yet, unavailable\n * means the surface could not show it even if something had. Conflating the two\n * is how a person concludes a working feature is broken, or a missing one is\n * merely quiet.\n */\nexport function Unavailable({ what, because, wouldNeed }) {\n return (_jsxs(\"div\", { className: C.unavailable, \"data-watch-unavailable\": \"\", children: [_jsxs(\"div\", { className: C.unavailableHead, children: [_jsx(\"h3\", { children: what }), _jsx(\"span\", { className: C.unavailableBadge, children: \"Not available in this build\" })] }), _jsx(\"p\", { className: C.unavailableBecause, children: because }), wouldNeed.length === 0 ? null : (_jsxs(_Fragment, { children: [_jsx(\"h4\", { className: C.panelHeading, children: \"What it would take\" }), _jsx(\"ul\", { className: C.requirements, children: wouldNeed.map(item => _jsx(\"li\", { children: item }, item)) })] }))] }));\n}\n/**\n * Read the JSON a Watch tool returned out of whatever DSH handed us.\n *\n * Returns null on anything unexpected — a running call, a failed one, a result\n * that is not JSON, a shape we do not recognise. The caller then renders its\n * empty state, which is the honest outcome: a surface that cannot read its\n * input must not draw a card implying it did.\n */\nexport function readToolResult(value) {\n if (typeof value !== 'object' || value === null)\n return null;\n const block = value;\n if (!('kind' in block) || block.isError === true)\n return null;\n if (!Array.isArray(block.content))\n return null;\n const text = block.content\n .filter((part) => typeof part === 'object' && part !== null\n && part.type === 'text'\n && typeof part.text === 'string')\n .map(part => part.text)\n .join('');\n if (text === '')\n return null;\n try {\n return JSON.parse(text);\n }\n catch {\n return null;\n }\n}\n//# sourceMappingURL=surface.js.map","/**\n * The sources Live can offer, and what each one honestly is.\n *\n * A source appears here whether or not this machine can run it, because a\n * catalogue that hid unavailable sources would answer the wrong question: a\n * person wants to know what the product can observe *and* why it cannot observe\n * it here. Hiding the second is how \"the feature does not exist\" and \"your\n * machine cannot do it\" become indistinguishable.\n *\n * Two entries deserve their separation. **Browser Observer** watches a page and\n * records what it showed; **Browser Operator** acts on one and returns a\n * receipt for what it did. They are listed apart because they are apart: one\n * capability that covered both would grant the power to act while a person\n * believed they were enabling the power to watch, and no amount of wording in a\n * tooltip fixes a control that does two things.\n *\n * @module @deepwatch/dsh-live/sources-catalogue\n */\n\nimport type { SourceAvailability } from './capture.js'\n\n/** How a source is reached, which decides what it needs to run. */\nexport type SourceRuntime =\n /** Provided by the browser or the Electron shell. */\n | 'shell'\n /** A device attached to this machine. */\n | 'device'\n /** A process this machine starts. */\n | 'process'\n /** Synthetic, for tests. Talks to nothing. */\n | 'fixture'\n\nexport interface LiveSource {\n readonly id: string\n readonly name: string\n /** What it observes, in a sentence. */\n readonly what: string\n /** When it would ask, so nobody is surprised by a prompt. */\n readonly asks: string\n readonly runtime: SourceRuntime\n /** Whether using it needs an OS permission at all. */\n readonly needsOsPermission: boolean\n /**\n * Whether it can act on the world rather than only record it.\n *\n * Exactly one source is `true`, and it is the one whose name says so.\n */\n readonly canAct: boolean\n}\n\nexport const SOURCES: readonly LiveSource[] = [\n {\n id: 'screen',\n name: 'Screen',\n what: 'The whole display, as a continuous observation.',\n asks: 'Asks for screen-capture permission the first time you start it.',\n runtime: 'shell',\n needsOsPermission: true,\n canAct: false,\n },\n {\n id: 'window',\n name: 'Window',\n what: 'One application window rather than the whole display.',\n asks: 'Asks for screen-capture permission the first time you start it.',\n runtime: 'shell',\n needsOsPermission: true,\n canAct: false,\n },\n {\n id: 'camera',\n name: 'Camera',\n what: 'Live visual input from an attached device.',\n asks: 'Asks for camera permission the first time you start it.',\n runtime: 'device',\n needsOsPermission: true,\n canAct: false,\n },\n {\n id: 'microphone',\n name: 'Microphone',\n what: 'Live audio, with timings a citation can point at.',\n asks: 'Asks for microphone permission the first time you start it.',\n runtime: 'device',\n needsOsPermission: true,\n canAct: false,\n },\n {\n id: 'browser-observer',\n name: 'Browser Observer',\n what: 'Watches a page and records what it showed. Takes no action.',\n asks: 'No OS permission. Needs the browser runtime enabled.',\n runtime: 'process',\n needsOsPermission: false,\n canAct: false,\n },\n {\n id: 'browser-operator',\n name: 'Browser Operator',\n what: 'Acts on a page and returns a receipt for what it did. A separate capability from observing.',\n asks: 'No OS permission. Needs the browser runtime enabled, and every side effect carries an idempotency key.',\n runtime: 'process',\n needsOsPermission: false,\n canAct: true,\n },\n {\n id: 'synthetic',\n name: 'Synthetic source',\n what: 'A task-owned page this workspace generates, for exercising capture without touching anything of yours.',\n asks: 'No permission. It observes only content this workspace created.',\n runtime: 'fixture',\n needsOsPermission: false,\n canAct: false,\n },\n]\n\n/** One source by id, or undefined. */\nexport function sourceById(id: string): LiveSource | undefined {\n return SOURCES.find(source => source.id === id)\n}\n\n/**\n * A short, honest word for how a source stands right now.\n *\n * Availability is a runtime fact, so the catalogue never claims it. Without an\n * adapter's probe, the truthful answer is that nothing has been started —\n * which is different from unavailable, and different again from broken.\n */\nexport function describeAvailability(availability?: SourceAvailability): string {\n if (availability === undefined) return 'Not started'\n return availability.available ? 'Available' : availability.reason\n}\n\n/**\n * The synthetic adapter's availability.\n *\n * Always available, because it invents its own content and reaches nothing.\n * This is the source the end-to-end capture test uses: exercising the real\n * lifecycle without capturing a single pixel of anybody's actual screen.\n */\nexport function syntheticAvailability(): SourceAvailability {\n return { available: true, reason: '' }\n}\n","/**\n * The Live mode body.\n *\n * It lives in the package that owns the capability rather than in the workspace\n * shell. That is structural, not tidiness: the shell provides the scaffold every\n * mode shares, and a mode that also needed something back from its own package\n * made the two depend on each other. TypeScript refused the circular project\n * reference, which was the right answer to the wrong arrangement.\n *\n * Nothing here asks the operating system for anything. Rendering the source list\n * calls no probe and no permission API; a prompt on page load teaches people to\n * click Allow without reading, and after that the prompt means nothing.\n *\n * @module @deepwatch/dsh-live/client/live-mode\n */\n\nimport type { ReactNode } from 'react'\nimport { EmptyState, Facts, ModeSurface, Panel, Unavailable } from '@deepwatch/dsh-workspace/surface'\nimport type { ModeViewProps } from '@deepwatch/dsh-workspace/surface'\nimport { SOURCES } from '../sources-catalogue.js'\nimport type { CaptureReceipt, CaptureState, Observation, PermissionState } from '../capture.js'\n\n/** A snapshot of a session, flat enough to render without owning the session. */\nexport interface LiveSessionView {\n readonly sessionId: string\n readonly sourceId: string\n readonly state: CaptureState\n readonly permission: PermissionState\n readonly runId: string | null\n readonly startedAt: string | null\n readonly observations: readonly Observation[]\n readonly reason: string\n}\n\nexport interface LiveModeProps extends ModeViewProps {\n /** The session in progress, when there is one. */\n readonly session?: LiveSessionView | null\n /** Receipts from sessions that have ended, newest first. */\n readonly receipts?: readonly CaptureReceipt[]\n}\n\n/** Words for a state, so it is never only a colour. */\nconst STATE_WORDS: Record<CaptureState, string> = {\n idle: 'Not started',\n requesting_permission: 'Waiting for your permission',\n starting: 'Starting',\n active: 'Observing',\n paused: 'Paused',\n stopping: 'Stopping',\n stopped: 'Stopped',\n cancelled: 'Cancelled',\n denied: 'Permission refused — nothing was captured',\n unavailable: 'Source unavailable',\n timed_out: 'The source did not start in time',\n failed: 'Failed',\n}\n\nconst PERMISSION_WORDS: Record<PermissionState, string> = {\n not_requested: 'Not requested',\n requested: 'Requested',\n granted: 'Granted',\n denied: 'Refused',\n}\n\nfunction toneFor(state: CaptureState): string {\n if (state === 'active') return 'var(--watch-tone-active)'\n if (['denied', 'failed', 'timed_out', 'unavailable'].includes(state)) return 'var(--watch-tone-error)'\n if (['paused', 'starting', 'requesting_permission', 'stopping'].includes(state)) return 'var(--watch-tone-caution)'\n return 'var(--watch-tone-neutral)'\n}\n\n/** The Live mode: what could be observed, and what is being observed. */\nexport function LiveModeView({ session = null, receipts = [] }: LiveModeProps = {}): ReactNode {\n return (\n <ModeSurface\n title=\"Live\"\n lead={\n 'A continuous observation over a source, with its clock, its gaps and '\n + 'its freshness. Opening this page starts nothing and asks for nothing.'\n }\n >\n {session === null\n ? (\n <EmptyState\n shows={\n 'An active session: which source is bound, its permission state, '\n + 'the observations arriving with their timestamps, and how it ended.'\n }\n why=\"No live session is running, and no source has been started.\"\n next={[\n 'Choose a source below and start it — each asks for its own permission at that moment, not before.',\n 'Bind ASR or Visual Perception in Settings → Role Bindings if you want a source interpreted rather than only recorded.',\n ]}\n />\n )\n : (\n <>\n <Panel>\n <div style={{ display: 'flex', alignItems: 'baseline', gap: '10px', flexWrap: 'wrap' }}>\n <strong style={{ fontSize: '15px', color: toneFor(session.state) }}>\n {STATE_WORDS[session.state]}\n </strong>\n <span style={{ fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' }}>\n {session.reason === '' ? session.sourceId : session.reason}\n </span>\n </div>\n <div style={{ marginTop: '12px' }}>\n <Facts\n rows={[\n ['Session', <span key=\"s\" data-watch-ltr>{session.sessionId}</span>],\n ['Source', session.sourceId],\n ['Permission', PERMISSION_WORDS[session.permission]],\n ['Run', session.runId ?? 'Not associated with a run'],\n ['Started', <span key=\"t\" data-watch-ltr>{session.startedAt ?? '—'}</span>],\n ['Observations', String(session.observations.length)],\n ]}\n />\n </div>\n </Panel>\n\n <Panel heading={`Observations (${String(session.observations.length)})`}>\n {session.observations.length === 0\n ? (\n <p style={{ fontSize: '13px', margin: 0, color: 'var(--dsw-alias-label-tertiary)' }}>\n Nothing observed yet. An observation without a timestamp\n could not be cited, so none is recorded until the clock\n is running.\n </p>\n )\n : (\n <ol style={{ margin: 0, paddingInlineStart: '20px', fontSize: '12.5px', lineHeight: 1.7 }}>\n {session.observations.slice(-25).map(observation => (\n <li key={observation.observationId}>\n <span data-watch-ltr style={{ color: 'var(--dsw-alias-label-tertiary)' }}>\n {`+${String(observation.offsetMs)}ms`}\n </span>\n {' '}\n {observation.text}\n </li>\n ))}\n </ol>\n )}\n </Panel>\n </>\n )}\n\n <Panel heading=\"Sources\">\n <Facts\n rows={SOURCES.map(source => [\n source.name,\n <span key={source.id}>\n {source.what}\n <br />\n <span style={{ color: 'var(--dsw-alias-label-tertiary)' }}>{source.asks}</span>\n {source.canAct\n ? (\n <>\n <br />\n <span style={{ color: 'var(--watch-tone-caution)' }}>\n This one can act on the world, not only record it.\n </span>\n </>\n )\n : null}\n </span>,\n ])}\n />\n </Panel>\n\n {receipts.length === 0\n ? null\n : (\n <Panel heading={`Finished sessions (${String(receipts.length)})`}>\n <Facts\n rows={receipts.slice(0, 10).map(receipt => [\n receipt.sessionId,\n <span key={receipt.sessionId}>\n {STATE_WORDS[receipt.finalState]}\n {` · ${String(receipt.observationCount)} observation(s)`}\n {receipt.reason === '' ? '' : ` · ${receipt.reason}`}\n </span>,\n ])}\n />\n </Panel>\n )}\n\n <Unavailable\n what=\"Sources this machine cannot provide\"\n because={\n 'Every source above keeps its real adapter and its real permission '\n + 'boundary, and each is exercised deterministically. A source is only '\n + 'offered as startable where its adapter can actually run here — a '\n + 'control that fails when pressed teaches people the product is '\n + 'broken rather than that a capability is absent.'\n }\n wouldNeed={[\n 'The hardware or OS support the adapter names, on the machine running Watch.',\n 'The relevant OS permission, granted at first use rather than on load.',\n 'A role bound for interpretation if the stream is to be transcribed or described.',\n ]}\n />\n </ModeSurface>\n )\n}\n","/**\n * Live: watching something while it is still happening.\n *\n * The engine already produces live observations. What did not exist was the\n * client half — and the client half is where every interesting failure lives,\n * because a live surface has to be correct about time it did not see.\n *\n * Four decisions carry this module.\n *\n * **A cursor is the only ordering authority.** Events are read by cursor, and\n * a repeated cursor returns the same events. That makes a retry free: a caller\n * that times out, reconnects and re-asks cannot lose an event or receive one\n * twice, which is what makes a flaky network a nuisance instead of a source of\n * fabricated history.\n *\n * **A gap is a fact, not a rendering problem.** When a delta does not continue\n * from the cursor the client holds, the client does not interpolate, does not\n * guess, and does not quietly skip forward. It records a gap with its own\n * range and asks for a fresh snapshot. The gap stays in the buffer afterwards.\n * Every alternative — smoothing, backfilling, \"probably nothing happened\" —\n * converts missing evidence into evidence, which is the one thing this product\n * cannot do.\n *\n * **Three clocks, never one.** Wall clock, media clock and session clock\n * disagree constantly during a live observation, and each answers a different\n * question: when did this happen in the world, where is it in the source, and\n * how far into watching are we. Collapsing them produces a subtitle attributed\n * to the wrong second.\n *\n * **The buffer is bounded.** A session left running for eight hours must not\n * grow a browser tab without limit. Trimming drops the oldest ordinary events\n * and keeps three things forever: gaps, pinned moments, and the count of what\n * was trimmed. A buffer that silently forgot a gap would let a long session\n * end up looking cleaner than a short one.\n *\n * @module @deepwatch/dsh-live/session\n */\n\nimport type { TemporalRange } from '@deepwatch/dsh-contracts'\n\n/** What a live session is watching. */\nexport type LiveKind = 'file_replay' | 'stream' | 'browser'\n\n/** What kind of thing one live event is. */\nexport type LiveEventKind =\n | 'speech'\n | 'ocr'\n | 'frame'\n | 'detector'\n | 'status'\n /** Capture stopped. Produced by the engine, or minted locally on a cursor break. */\n | 'gap'\n\n/** One thing observed, as the engine reports it. */\nexport interface LiveEvent {\n /** Monotonic within the session. */\n readonly seq: number\n /** The cursor *after* this event, for resuming. */\n readonly cursor: string\n readonly kind: LiveEventKind\n /** Wall clock, epoch milliseconds. */\n readonly at: number\n /** Position on the source's own clock. Null for events with no media position. */\n readonly mediaMs: number | null\n /** One line. Presentation only. */\n readonly text: string\n /** Range this event covers, for a gap or a span. */\n readonly range: TemporalRange | null\n /** Evidence this event resolves to, when the engine minted any. */\n readonly evidenceIds: readonly string[]\n}\n\n/** Connection state of the observation channel. */\nexport type ConnectionState = 'connecting' | 'live' | 'reconnecting' | 'lost' | 'stopped'\n\n/** Whether the session itself is running. */\nexport type LiveStatus = 'starting' | 'observing' | 'stopping' | 'finalized' | 'discarded'\n\n/** The three clocks, read together so they cannot drift apart in the UI. */\nexport interface LiveClocks {\n /** Epoch milliseconds of the most recent event. Null before the first. */\n readonly wallMs: number | null\n /** Position in the source. Null for a source with no media clock. */\n readonly mediaMs: number | null\n /** Milliseconds since the session started. */\n readonly sessionMs: number\n /**\n * Observation latency: how far behind the wall clock the newest event is.\n *\n * Reported rather than corrected. A live view that hid its own lag would\n * make \"what is on screen right now\" mean something it does not.\n */\n readonly latencyMs: number | null\n}\n\n/** A moment somebody chose to keep. */\nexport interface PinnedMoment {\n readonly momentId: string\n readonly atMediaMs: number | null\n readonly atWallMs: number\n readonly note: string\n readonly evidenceIds: readonly string[]\n}\n\n/** Everything the Live surface renders. */\nexport interface LiveSessionState {\n readonly sessionId: string\n readonly target: string\n readonly kind: LiveKind\n readonly status: LiveStatus\n readonly connection: ConnectionState\n /** The cursor to resume from. Empty before the first read. */\n readonly cursor: string\n readonly startedAtMs: number\n readonly events: readonly LiveEvent[]\n /** Every gap, kept whatever the buffer drops. */\n readonly gaps: readonly TemporalRange[]\n readonly pinned: readonly PinnedMoment[]\n readonly clocks: LiveClocks\n /** How many ordinary events the bound dropped. */\n readonly trimmed: number\n /**\n * Set when the client knows its view is not continuous.\n *\n * Cleared only by a fresh snapshot. Nothing else clears it, because nothing\n * else re-establishes continuity.\n */\n readonly needsSnapshot: boolean\n /** Why the last read failed, when one did. */\n readonly lastError: string | null\n}\n\n/** How large a session buffer may grow. */\nexport interface BufferLimits {\n /** Ordinary events kept. Gaps and pinned moments are exempt. */\n readonly maxEvents: number\n}\n\n/** The default bound. Roughly an hour of dense observation. */\nexport const DEFAULT_LIMITS: BufferLimits = { maxEvents: 2000 }\n\n/** Begin a session, before anything has been observed. */\nexport function startSession(input: {\n readonly sessionId: string\n readonly target: string\n readonly kind: LiveKind\n readonly startedAtMs: number\n}): LiveSessionState {\n return {\n sessionId: input.sessionId,\n target: input.target,\n kind: input.kind,\n status: 'starting',\n connection: 'connecting',\n cursor: '',\n startedAtMs: input.startedAtMs,\n events: [],\n gaps: [],\n pinned: [],\n clocks: { wallMs: null, mediaMs: null, sessionMs: 0, latencyMs: null },\n trimmed: 0,\n needsSnapshot: false,\n lastError: null,\n }\n}\n\n/** What one read of the observation channel returned. */\nexport interface LiveDelta {\n /** The cursor this delta continues from. Empty means \"from the beginning\". */\n readonly fromCursor: string\n readonly nextCursor: string\n readonly events: readonly LiveEvent[]\n readonly status: LiveStatus\n /**\n * True when the engine is answering a snapshot request rather than a delta.\n *\n * A snapshot replaces the client's continuity assumption; a delta extends\n * it. Conflating them is how a reconnect ends up appending the beginning of\n * the session to the end of it.\n */\n readonly isSnapshot: boolean\n /** Gaps the engine itself reported. */\n readonly gaps?: readonly TemporalRange[]\n}\n\n/** Recompute the clocks from the newest event. */\nfunction clocksFrom(\n state: LiveSessionState,\n events: readonly LiveEvent[],\n nowMs: number,\n): LiveClocks {\n const newest = events[events.length - 1]\n if (newest === undefined) {\n return { wallMs: null, mediaMs: null, sessionMs: nowMs - state.startedAtMs, latencyMs: null }\n }\n return {\n wallMs: newest.at,\n mediaMs: newest.mediaMs,\n sessionMs: nowMs - state.startedAtMs,\n // Never negative. A clock skew that made the newest event look like it\n // arrived from the future would render as a nonsense latency.\n latencyMs: Math.max(0, nowMs - newest.at),\n }\n}\n\n/**\n * Trim the buffer to its bound.\n *\n * Gaps and pinned events survive. The trim count is carried so the surface can\n * say \"1,204 earlier events dropped\" rather than presenting a partial history\n * as a complete one.\n */\nfunction bound(\n events: readonly LiveEvent[],\n pinned: readonly PinnedMoment[],\n limits: BufferLimits,\n): { readonly events: readonly LiveEvent[]; readonly dropped: number } {\n if (events.length <= limits.maxEvents) return { events, dropped: 0 }\n\n const pinnedEvidence = new Set(pinned.flatMap(moment => moment.evidenceIds))\n const keepAlways = (event: LiveEvent): boolean =>\n event.kind === 'gap' || event.evidenceIds.some(id => pinnedEvidence.has(id))\n\n const protectedEvents = events.filter(keepAlways)\n const ordinary = events.filter(event => !keepAlways(event))\n const room = Math.max(0, limits.maxEvents - protectedEvents.length)\n const kept = ordinary.slice(Math.max(0, ordinary.length - room))\n\n const keptIds = new Set([...protectedEvents, ...kept].map(event => event.seq))\n return {\n events: events.filter(event => keptIds.has(event.seq)),\n dropped: ordinary.length - kept.length,\n }\n}\n\n/** Mint the gap a cursor break implies, with the range it actually covers. */\nfunction cursorBreakGap(state: LiveSessionState, delta: LiveDelta): LiveEvent | null {\n const last = state.events[state.events.length - 1]\n const next = delta.events[0]\n if (last === undefined || next === undefined) return null\n return {\n seq: last.seq + 0.5,\n cursor: state.cursor,\n kind: 'gap',\n at: last.at,\n mediaMs: last.mediaMs,\n text: 'observation gap — the stream did not continue from the last cursor',\n range: last.mediaMs === null || next.mediaMs === null\n ? null\n : { startMs: last.mediaMs, endMs: next.mediaMs },\n evidenceIds: [],\n }\n}\n\n/**\n * Apply one delta or snapshot to the session.\n *\n * The three cases, in the order they are checked:\n *\n * 1. **A snapshot** replaces the event view outright and clears\n * `needsSnapshot`. This is the only thing that re-establishes continuity.\n * 2. **A delta that continues from the held cursor** appends. This is the\n * ordinary case.\n * 3. **A delta that does not** is a break. A gap event is minted at the seam,\n * `needsSnapshot` is set, and the delta's events are *not* appended —\n * appending them would splice two discontinuous stretches into something\n * that reads as continuous.\n *\n * Re-applying the same delta is a no-op, which is what makes a retry safe.\n */\nexport function applyDelta(\n state: LiveSessionState,\n delta: LiveDelta,\n nowMs: number,\n limits: BufferLimits = DEFAULT_LIMITS,\n): LiveSessionState {\n const engineGaps = delta.gaps ?? []\n\n if (delta.isSnapshot) {\n const { events, dropped } = bound(delta.events, state.pinned, limits)\n return {\n ...state,\n status: delta.status,\n connection: delta.status === 'observing' ? 'live' : state.connection,\n cursor: delta.nextCursor,\n events,\n gaps: mergeGaps(state.gaps, [...engineGaps, ...rangesOf(delta.events)]),\n clocks: clocksFrom(state, events, nowMs),\n trimmed: state.trimmed + dropped,\n needsSnapshot: false,\n lastError: null,\n }\n }\n\n // An already-applied delta. Returning the same state is what makes a retry\n // after a timeout free rather than a source of duplicates.\n if (delta.nextCursor === state.cursor) return state\n\n if (delta.fromCursor !== state.cursor) {\n const gap = cursorBreakGap(state, delta)\n const events = gap === null ? state.events : [...state.events, gap]\n return {\n ...state,\n connection: 'reconnecting',\n events,\n gaps: mergeGaps(state.gaps, gap?.range === null || gap?.range === undefined ? [] : [gap.range]),\n clocks: clocksFrom(state, events, nowMs),\n needsSnapshot: true,\n lastError: `cursor ${delta.fromCursor} does not continue from `\n + (state.cursor === '' ? '(start)' : state.cursor),\n }\n }\n\n const appended = [...state.events, ...delta.events]\n const { events, dropped } = bound(appended, state.pinned, limits)\n return {\n ...state,\n status: delta.status,\n connection: delta.status === 'observing' ? 'live' : state.connection,\n cursor: delta.nextCursor,\n events,\n gaps: mergeGaps(state.gaps, [...engineGaps, ...rangesOf(delta.events)]),\n clocks: clocksFrom(state, events, nowMs),\n trimmed: state.trimmed + dropped,\n lastError: null,\n }\n}\n\n/** Ranges of the gap events in a batch. */\nfunction rangesOf(events: readonly LiveEvent[]): readonly TemporalRange[] {\n return events\n .filter(event => event.kind === 'gap' && event.range !== null)\n .map(event => event.range as TemporalRange)\n}\n\n/** Union of two gap lists, deduplicated and ordered. */\nfunction mergeGaps(\n existing: readonly TemporalRange[],\n incoming: readonly TemporalRange[],\n): readonly TemporalRange[] {\n const seen = new Map<string, TemporalRange>()\n for (const range of [...existing, ...incoming]) {\n seen.set(`${String(range.startMs)}:${String(range.endMs)}`, range)\n }\n return [...seen.values()].sort((left, right) => left.startMs - right.startMs)\n}\n\n/** Record that the channel dropped. */\nexport function connectionLost(state: LiveSessionState, reason: string): LiveSessionState {\n return {\n ...state,\n connection: 'lost',\n // A lost connection always needs a snapshot on return. Even if no event\n // was missed, the client cannot know that, and assuming it is how a gap\n // goes unrecorded.\n needsSnapshot: true,\n lastError: reason,\n }\n}\n\n/** Record that a reconnect attempt is under way. */\nexport function reconnecting(state: LiveSessionState): LiveSessionState {\n return { ...state, connection: 'reconnecting' }\n}\n\n/**\n * Pin a moment.\n *\n * Pinning protects the events it references from being trimmed. That is the\n * whole reason pins take evidence ids: a pin that pointed only at a timestamp\n * would survive a trim while the thing it pointed at did not.\n */\nexport function pinMoment(\n state: LiveSessionState,\n moment: PinnedMoment,\n): LiveSessionState {\n if (state.pinned.some(existing => existing.momentId === moment.momentId)) return state\n return { ...state, pinned: [...state.pinned, moment] }\n}\n\n/** Stop the session, keeping or discarding what it observed. */\nexport function finish(state: LiveSessionState, finalize: boolean): LiveSessionState {\n return {\n ...state,\n status: finalize ? 'finalized' : 'discarded',\n connection: 'stopped',\n }\n}\n\n/**\n * The replayable record of a finished session.\n *\n * A finalized session has to reopen as what it was. This is the value that\n * makes that true — and it deliberately keeps `trimmed`, so a replay of a long\n * session says how much of it the live view did not retain rather than\n * presenting what survived as the whole thing.\n */\nexport interface LiveReplay {\n readonly sessionId: string\n readonly target: string\n readonly kind: LiveKind\n readonly status: LiveStatus\n readonly events: readonly LiveEvent[]\n readonly gaps: readonly TemporalRange[]\n readonly pinned: readonly PinnedMoment[]\n readonly trimmed: number\n}\n\n/** Freeze a finished session into its replay record. */\nexport function toReplay(state: LiveSessionState): LiveReplay {\n return {\n sessionId: state.sessionId,\n target: state.target,\n kind: state.kind,\n status: state.status,\n events: state.events,\n gaps: state.gaps,\n pinned: state.pinned,\n trimmed: state.trimmed,\n }\n}\n\n/**\n * Digest of a replay, so \"reopening it shows the same thing\" is assertable.\n *\n * FNV-1a over event identity and gap ranges — the same construction the\n * trajectory projection uses, and for the same reason.\n */\nexport function replayDigest(replay: LiveReplay): string {\n let hash = 0x811c9dc5\n const feed = (text: string): void => {\n for (let index = 0; index < text.length; index += 1) {\n hash ^= text.charCodeAt(index)\n hash = Math.imul(hash, 0x01000193) >>> 0\n }\n }\n feed(`${replay.sessionId}|${replay.status}|${String(replay.trimmed)}`)\n for (const event of replay.events) feed(`|${String(event.seq)}:${event.kind}:${event.cursor}`)\n for (const gap of replay.gaps) feed(`|gap:${String(gap.startMs)}-${String(gap.endMs)}`)\n return hash.toString(16).padStart(8, '0')\n}\n\n/**\n * One line describing the session's honesty, for the surface header.\n *\n * Always says the gap count and the trim count, including when both are zero.\n * A message that appears only when something is wrong is a message people\n * learn to not look for.\n */\nexport function describeContinuity(state: LiveSessionState): string {\n const parts = [\n state.gaps.length === 0 ? 'no gaps' : `${String(state.gaps.length)} gap(s)`,\n state.trimmed === 0 ? 'nothing dropped' : `${String(state.trimmed)} earlier event(s) dropped`,\n ]\n if (state.needsSnapshot) parts.push('view not continuous — resnapshot pending')\n return parts.join(' · ')\n}\n","/**\n * The Live surface.\n *\n * The header is deliberately busy. A live view's job is to be trustworthy\n * about time, and that means the clocks, the latency, the connection state and\n * the continuity line are all present at once rather than behind a disclosure.\n * The moment one of them is hidden, the surface starts implying a continuity\n * it has not checked.\n *\n * @module @deepwatch/dsh-live/components\n */\n\nimport type { ReactNode } from 'react'\nimport { toneFor, tokenFor } from '@deepwatch/dsh-client-brand'\nimport {\n describeContinuity,\n type LiveEvent,\n type LiveSessionState,\n} from '../session.js'\n\n/** Connection states, mapped onto the brand's status vocabulary. */\nconst CONNECTION_STATUS: Readonly<Record<string, string>> = {\n connecting: 'queued',\n live: 'running',\n reconnecting: 'gap',\n lost: 'unavailable',\n stopped: 'completed',\n}\n\n/** A millisecond count as a clock reading. */\nfunction clock(ms: number | null): string {\n if (ms === null) return '—'\n const total = Math.max(0, Math.floor(ms / 1000))\n const seconds = String(total % 60).padStart(2, '0')\n const minutes = String(Math.floor(total / 60) % 60).padStart(2, '0')\n const hours = String(Math.floor(total / 3600)).padStart(2, '0')\n return `${hours}:${minutes}:${seconds}`\n}\n\n/** Props for {@link LiveHeader}. */\nexport interface LiveHeaderProps {\n readonly state: LiveSessionState\n}\n\n/**\n * Session, media and wall clocks, side by side.\n *\n * Three readings rather than one, because they answer three questions and\n * disagree constantly during a live observation. A single \"time\" field would\n * be right about one of them and quietly wrong about the other two.\n */\nexport function LiveHeader({ state }: LiveHeaderProps): ReactNode {\n const status = CONNECTION_STATUS[state.connection] ?? 'unavailable'\n return (\n <header data-watch-live-header=\"\" data-watch-live-session={state.sessionId}>\n <span data-watch-field=\"target\" dir=\"ltr\">{state.target}</span>\n <span data-watch-field=\"kind\">{state.kind}</span>\n <span data-watch-field=\"status\">{state.status}</span>\n <span data-watch-field=\"connection\" style={{ color: tokenFor(toneFor(status)) }}>\n <span aria-hidden=\"true\">{state.connection === 'live' ? '●' : '▲'}</span>\n <span>{state.connection}</span>\n </span>\n <span data-watch-field=\"session-clock\" dir=\"ltr\">{`session ${clock(state.clocks.sessionMs)}`}</span>\n <span data-watch-field=\"media-clock\" dir=\"ltr\">{`media ${clock(state.clocks.mediaMs)}`}</span>\n <span data-watch-field=\"wall-clock\" dir=\"ltr\">\n {state.clocks.wallMs === null ? 'wall —' : `wall ${new Date(state.clocks.wallMs).toISOString()}`}\n </span>\n <span data-watch-field=\"latency\">\n {state.clocks.latencyMs === null ? 'latency —' : `latency ${String(state.clocks.latencyMs)}ms`}\n </span>\n <span data-watch-field=\"continuity\">{describeContinuity(state)}</span>\n </header>\n )\n}\n\n/** Props for {@link LiveEventRow}. */\nexport interface LiveEventRowProps {\n readonly event: LiveEvent\n readonly onSelect: (event: LiveEvent) => void\n}\n\n/**\n * One observed event.\n *\n * A gap draws as a gap — dashed, glyphed and labelled — and is never\n * collapsed into the events around it.\n */\nexport function LiveEventRow({ event, onSelect }: LiveEventRowProps): ReactNode {\n const isGap = event.kind === 'gap'\n return (\n <li data-watch-live-event={event.kind} data-watch-seq={String(event.seq)}>\n <button\n type=\"button\"\n onClick={() => { onSelect(event) }}\n style={{\n font: 'inherit',\n color: 'inherit',\n cursor: 'pointer',\n background: isGap ? 'var(--watch-wash-caution)' : 'none',\n border: isGap ? '1px dashed var(--watch-tone-caution)' : '1px solid transparent',\n textAlign: 'start',\n width: '100%',\n }}\n >\n <span dir=\"ltr\" style={{ fontVariantNumeric: 'tabular-nums' }}>{clock(event.mediaMs)}</span>\n {isGap && <span aria-hidden=\"true\">{' ⌇ '}</span>}\n <span dir=\"auto\">{event.text}</span>\n </button>\n </li>\n )\n}\n\n/** Props for {@link LiveSurface}. */\nexport interface LiveSurfaceProps {\n readonly state: LiveSessionState\n readonly onStart: () => void\n readonly onStop: (finalize: boolean) => void\n readonly onAsk: (question: string) => void\n readonly onSelect: (event: LiveEvent) => void\n readonly onPin: () => void\n}\n\n/**\n * The Live mode body.\n *\n * `Resnapshot needed` is rendered as a banner rather than a toast. A transient\n * notification for \"your view of this is not continuous\" is a notification\n * that will be missed exactly when it matters.\n */\nexport function LiveSurface(props: LiveSurfaceProps): ReactNode {\n const { state } = props\n return (\n <section data-watch-live=\"\" aria-label=\"Live observation\">\n <LiveHeader state={state} />\n {state.needsSnapshot && (\n <p\n role=\"alert\"\n data-watch-live-resnapshot=\"\"\n style={{ borderInlineStart: '3px solid var(--watch-tone-caution)', paddingInlineStart: '8px' }}\n >\n {'This view is not continuous. '}\n {state.lastError ?? 'The stream did not continue from the last cursor.'}\n {' A fresh snapshot is needed before anything here can be read as unbroken.'}\n </p>\n )}\n <div data-watch-live-controls=\"\">\n <button type=\"button\" data-watch-action=\"start\" onClick={props.onStart}>Start</button>\n <button type=\"button\" data-watch-action=\"pin\" onClick={props.onPin}>Pin moment</button>\n <button type=\"button\" data-watch-action=\"finalize\" onClick={() => { props.onStop(true) }}>\n Stop and keep\n </button>\n <button type=\"button\" data-watch-action=\"discard\" onClick={() => { props.onStop(false) }}>\n Stop and discard\n </button>\n </div>\n <ul data-watch-live-events=\"\" style={{ listStyle: 'none', margin: 0, padding: 0 }}>\n {state.events.map(event => (\n <LiveEventRow key={String(event.seq)} event={event} onSelect={props.onSelect} />\n ))}\n </ul>\n {state.pinned.length > 0 && (\n <ul data-watch-live-pinned=\"\" aria-label=\"Pinned moments\">\n {state.pinned.map(moment => (\n <li key={moment.momentId} data-watch-pinned={moment.momentId}>\n <span dir=\"ltr\">{clock(moment.atMediaMs)}</span>\n <span dir=\"auto\">{` ${moment.note}`}</span>\n </li>\n ))}\n </ul>\n )}\n {state.trimmed > 0 && (\n <p data-watch-live-trimmed={String(state.trimmed)}>\n {`${String(state.trimmed)} earlier event(s) are no longer held in this view. `}\n {'Gaps and pinned moments were kept.'}\n </p>\n )}\n </section>\n )\n}\n","/**\n * The Live surface, registered into DSH's slots.\n *\n * @module @deepwatch/dsh-live/client\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport { LiveModeView } from './live-mode.js'\n\nexport * from './components.js'\nexport * from './live-mode.js'\nexport * from '../session.js'\n\n/** Services this half needs before it can register anything. */\nexport const inject = ['slots']\n\n/** The minimal shape of DSH's slot service this module uses. */\ninterface SlotService {\n inject(name: string, register: () => void): void\n register(entry: Record<string, unknown>, component: unknown): void\n}\n\n/** Register the Live mode body. */\nexport function apply(ctx: Context): void {\n const slots = (ctx as unknown as { slots: SlotService }).slots\n // Live is a product mode, so it registers as one of DSH's views. The session\n // header turns the registered set into its own tab strip, which is why this\n // is a view rather than a panel Watch would have to place and style itself.\n slots.inject('conversation.view', () => {\n slots.register({ name: 'conversation.view', id: 'live', label: 'Live', order: 30 }, LiveModeView)\n })\n}\n"],"mappings":"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"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Live mode: the client half of watching something while it happens.
3
+ *
4
+ * @module @deepwatch/dsh-live
5
+ */
6
+ export * from './session.js';
7
+ export * from './capture.js';
8
+ export * from './sources-catalogue.js';
9
+ export * from './synthetic-source.js';
10
+ export * from './triggers.js';
11
+ /**
12
+ * The host-side loader entry.
13
+ *
14
+ * There is no host behaviour to install — this package's product surface is
15
+ * entirely browser-side, under `./client`. The entry exists because the DSH
16
+ * loader mounts a package's node half first and reads its `dsh.client`
17
+ * declaration from there; a row whose module exports no `apply` is refused
18
+ * with "invalid plugin, expect function or object with an apply method", and
19
+ * the whole plugin tree fails to load with it.
20
+ */
21
+ export declare function apply(): void;
22
+ //# sourceMappingURL=index.d.ts.map
package/lib/index.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Live mode: the client half of watching something while it happens.
3
+ *
4
+ * @module @deepwatch/dsh-live
5
+ */
6
+ export * from './session.js';
7
+ export * from './capture.js';
8
+ export * from './sources-catalogue.js';
9
+ export * from './synthetic-source.js';
10
+ export * from './triggers.js';
11
+ /**
12
+ * The host-side loader entry.
13
+ *
14
+ * There is no host behaviour to install — this package's product surface is
15
+ * entirely browser-side, under `./client`. The entry exists because the DSH
16
+ * loader mounts a package's node half first and reads its `dsh.client`
17
+ * declaration from there; a row whose module exports no `apply` is refused
18
+ * with "invalid plugin, expect function or object with an apply method", and
19
+ * the whole plugin tree fails to load with it.
20
+ */
21
+ export function apply() { }
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Live: watching something while it is still happening.
3
+ *
4
+ * The engine already produces live observations. What did not exist was the
5
+ * client half — and the client half is where every interesting failure lives,
6
+ * because a live surface has to be correct about time it did not see.
7
+ *
8
+ * Four decisions carry this module.
9
+ *
10
+ * **A cursor is the only ordering authority.** Events are read by cursor, and
11
+ * a repeated cursor returns the same events. That makes a retry free: a caller
12
+ * that times out, reconnects and re-asks cannot lose an event or receive one
13
+ * twice, which is what makes a flaky network a nuisance instead of a source of
14
+ * fabricated history.
15
+ *
16
+ * **A gap is a fact, not a rendering problem.** When a delta does not continue
17
+ * from the cursor the client holds, the client does not interpolate, does not
18
+ * guess, and does not quietly skip forward. It records a gap with its own
19
+ * range and asks for a fresh snapshot. The gap stays in the buffer afterwards.
20
+ * Every alternative — smoothing, backfilling, "probably nothing happened" —
21
+ * converts missing evidence into evidence, which is the one thing this product
22
+ * cannot do.
23
+ *
24
+ * **Three clocks, never one.** Wall clock, media clock and session clock
25
+ * disagree constantly during a live observation, and each answers a different
26
+ * question: when did this happen in the world, where is it in the source, and
27
+ * how far into watching are we. Collapsing them produces a subtitle attributed
28
+ * to the wrong second.
29
+ *
30
+ * **The buffer is bounded.** A session left running for eight hours must not
31
+ * grow a browser tab without limit. Trimming drops the oldest ordinary events
32
+ * and keeps three things forever: gaps, pinned moments, and the count of what
33
+ * was trimmed. A buffer that silently forgot a gap would let a long session
34
+ * end up looking cleaner than a short one.
35
+ *
36
+ * @module @deepwatch/dsh-live/session
37
+ */
38
+ import type { TemporalRange } from '@deepwatch/dsh-contracts';
39
+ /** What a live session is watching. */
40
+ export type LiveKind = 'file_replay' | 'stream' | 'browser';
41
+ /** What kind of thing one live event is. */
42
+ export type LiveEventKind = 'speech' | 'ocr' | 'frame' | 'detector' | 'status'
43
+ /** Capture stopped. Produced by the engine, or minted locally on a cursor break. */
44
+ | 'gap';
45
+ /** One thing observed, as the engine reports it. */
46
+ export interface LiveEvent {
47
+ /** Monotonic within the session. */
48
+ readonly seq: number;
49
+ /** The cursor *after* this event, for resuming. */
50
+ readonly cursor: string;
51
+ readonly kind: LiveEventKind;
52
+ /** Wall clock, epoch milliseconds. */
53
+ readonly at: number;
54
+ /** Position on the source's own clock. Null for events with no media position. */
55
+ readonly mediaMs: number | null;
56
+ /** One line. Presentation only. */
57
+ readonly text: string;
58
+ /** Range this event covers, for a gap or a span. */
59
+ readonly range: TemporalRange | null;
60
+ /** Evidence this event resolves to, when the engine minted any. */
61
+ readonly evidenceIds: readonly string[];
62
+ }
63
+ /** Connection state of the observation channel. */
64
+ export type ConnectionState = 'connecting' | 'live' | 'reconnecting' | 'lost' | 'stopped';
65
+ /** Whether the session itself is running. */
66
+ export type LiveStatus = 'starting' | 'observing' | 'stopping' | 'finalized' | 'discarded';
67
+ /** The three clocks, read together so they cannot drift apart in the UI. */
68
+ export interface LiveClocks {
69
+ /** Epoch milliseconds of the most recent event. Null before the first. */
70
+ readonly wallMs: number | null;
71
+ /** Position in the source. Null for a source with no media clock. */
72
+ readonly mediaMs: number | null;
73
+ /** Milliseconds since the session started. */
74
+ readonly sessionMs: number;
75
+ /**
76
+ * Observation latency: how far behind the wall clock the newest event is.
77
+ *
78
+ * Reported rather than corrected. A live view that hid its own lag would
79
+ * make "what is on screen right now" mean something it does not.
80
+ */
81
+ readonly latencyMs: number | null;
82
+ }
83
+ /** A moment somebody chose to keep. */
84
+ export interface PinnedMoment {
85
+ readonly momentId: string;
86
+ readonly atMediaMs: number | null;
87
+ readonly atWallMs: number;
88
+ readonly note: string;
89
+ readonly evidenceIds: readonly string[];
90
+ }
91
+ /** Everything the Live surface renders. */
92
+ export interface LiveSessionState {
93
+ readonly sessionId: string;
94
+ readonly target: string;
95
+ readonly kind: LiveKind;
96
+ readonly status: LiveStatus;
97
+ readonly connection: ConnectionState;
98
+ /** The cursor to resume from. Empty before the first read. */
99
+ readonly cursor: string;
100
+ readonly startedAtMs: number;
101
+ readonly events: readonly LiveEvent[];
102
+ /** Every gap, kept whatever the buffer drops. */
103
+ readonly gaps: readonly TemporalRange[];
104
+ readonly pinned: readonly PinnedMoment[];
105
+ readonly clocks: LiveClocks;
106
+ /** How many ordinary events the bound dropped. */
107
+ readonly trimmed: number;
108
+ /**
109
+ * Set when the client knows its view is not continuous.
110
+ *
111
+ * Cleared only by a fresh snapshot. Nothing else clears it, because nothing
112
+ * else re-establishes continuity.
113
+ */
114
+ readonly needsSnapshot: boolean;
115
+ /** Why the last read failed, when one did. */
116
+ readonly lastError: string | null;
117
+ }
118
+ /** How large a session buffer may grow. */
119
+ export interface BufferLimits {
120
+ /** Ordinary events kept. Gaps and pinned moments are exempt. */
121
+ readonly maxEvents: number;
122
+ }
123
+ /** The default bound. Roughly an hour of dense observation. */
124
+ export declare const DEFAULT_LIMITS: BufferLimits;
125
+ /** Begin a session, before anything has been observed. */
126
+ export declare function startSession(input: {
127
+ readonly sessionId: string;
128
+ readonly target: string;
129
+ readonly kind: LiveKind;
130
+ readonly startedAtMs: number;
131
+ }): LiveSessionState;
132
+ /** What one read of the observation channel returned. */
133
+ export interface LiveDelta {
134
+ /** The cursor this delta continues from. Empty means "from the beginning". */
135
+ readonly fromCursor: string;
136
+ readonly nextCursor: string;
137
+ readonly events: readonly LiveEvent[];
138
+ readonly status: LiveStatus;
139
+ /**
140
+ * True when the engine is answering a snapshot request rather than a delta.
141
+ *
142
+ * A snapshot replaces the client's continuity assumption; a delta extends
143
+ * it. Conflating them is how a reconnect ends up appending the beginning of
144
+ * the session to the end of it.
145
+ */
146
+ readonly isSnapshot: boolean;
147
+ /** Gaps the engine itself reported. */
148
+ readonly gaps?: readonly TemporalRange[];
149
+ }
150
+ /**
151
+ * Apply one delta or snapshot to the session.
152
+ *
153
+ * The three cases, in the order they are checked:
154
+ *
155
+ * 1. **A snapshot** replaces the event view outright and clears
156
+ * `needsSnapshot`. This is the only thing that re-establishes continuity.
157
+ * 2. **A delta that continues from the held cursor** appends. This is the
158
+ * ordinary case.
159
+ * 3. **A delta that does not** is a break. A gap event is minted at the seam,
160
+ * `needsSnapshot` is set, and the delta's events are *not* appended —
161
+ * appending them would splice two discontinuous stretches into something
162
+ * that reads as continuous.
163
+ *
164
+ * Re-applying the same delta is a no-op, which is what makes a retry safe.
165
+ */
166
+ export declare function applyDelta(state: LiveSessionState, delta: LiveDelta, nowMs: number, limits?: BufferLimits): LiveSessionState;
167
+ /** Record that the channel dropped. */
168
+ export declare function connectionLost(state: LiveSessionState, reason: string): LiveSessionState;
169
+ /** Record that a reconnect attempt is under way. */
170
+ export declare function reconnecting(state: LiveSessionState): LiveSessionState;
171
+ /**
172
+ * Pin a moment.
173
+ *
174
+ * Pinning protects the events it references from being trimmed. That is the
175
+ * whole reason pins take evidence ids: a pin that pointed only at a timestamp
176
+ * would survive a trim while the thing it pointed at did not.
177
+ */
178
+ export declare function pinMoment(state: LiveSessionState, moment: PinnedMoment): LiveSessionState;
179
+ /** Stop the session, keeping or discarding what it observed. */
180
+ export declare function finish(state: LiveSessionState, finalize: boolean): LiveSessionState;
181
+ /**
182
+ * The replayable record of a finished session.
183
+ *
184
+ * A finalized session has to reopen as what it was. This is the value that
185
+ * makes that true — and it deliberately keeps `trimmed`, so a replay of a long
186
+ * session says how much of it the live view did not retain rather than
187
+ * presenting what survived as the whole thing.
188
+ */
189
+ export interface LiveReplay {
190
+ readonly sessionId: string;
191
+ readonly target: string;
192
+ readonly kind: LiveKind;
193
+ readonly status: LiveStatus;
194
+ readonly events: readonly LiveEvent[];
195
+ readonly gaps: readonly TemporalRange[];
196
+ readonly pinned: readonly PinnedMoment[];
197
+ readonly trimmed: number;
198
+ }
199
+ /** Freeze a finished session into its replay record. */
200
+ export declare function toReplay(state: LiveSessionState): LiveReplay;
201
+ /**
202
+ * Digest of a replay, so "reopening it shows the same thing" is assertable.
203
+ *
204
+ * FNV-1a over event identity and gap ranges — the same construction the
205
+ * trajectory projection uses, and for the same reason.
206
+ */
207
+ export declare function replayDigest(replay: LiveReplay): string;
208
+ /**
209
+ * One line describing the session's honesty, for the surface header.
210
+ *
211
+ * Always says the gap count and the trim count, including when both are zero.
212
+ * A message that appears only when something is wrong is a message people
213
+ * learn to not look for.
214
+ */
215
+ export declare function describeContinuity(state: LiveSessionState): string;
216
+ //# sourceMappingURL=session.d.ts.map