feedBack/static/capabilities/audio-session.js
Bret Mogilefsky af2949677a
rename: slopsmith → feedBack, byron → got-feedBack (#537)
* Update GitHub repo references from feedback* to feedBack*

* rename: slopsmith -> feedBack, byron -> got-feedBack

Renames across the entire codebase:
- slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack
- byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack
- /home/byron/ -> /opt/got-feedBack/
- byron@ougsoft.com -> hi@got-feedBack.org
- github.com/byrongamatos/ -> github.com/got-feedback/
- com.byron. -> com.got-feedback.
- SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback
- Protocol/storage strings migrated with read-old/write-new pattern
- window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias

Refs: #rename-slopsmith

* rename: complete regen against current main + fix backward-compat alias

Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of
current main (3 commits had landed since the branch: #572/#554/#574),
resolving the four content conflicts in favour of main's newer content
(autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge).

Completion fixes on top of the mechanical rename:
- Re-apply rename to post-branch content the original rename never saw:
  window.slopsmith(.Tour) consumers in lessons.js / notifications.js /
  onboarding-tour.js, and the matching JS + python tests (autoplay_exit,
  progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env
  vars now match server.py (which reads FEEDBACK_SYNC_STARTUP /
  FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real
  path again.
- Restore the window.slopsmith backward-compat alias dropped during conflict
  resolution, and move the bus aliases to AFTER the _feedBackExisting merge
  block so they reference the fully-assembled object (also fixes the
  loop_api.test.js API-surface regex, which the original PR latently broke).
- Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR)
  and gitignore it.
- Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped
  source labels.

Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669
passed / 1683 collected with 0 import errors; zero residual slopsmith/byron
except the two intentional window.slopsmith aliases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* rename: implement advertised backward-compat + prune dead community plugins

Address gaps where PR #537's "Backward compatibility" section was advertised
but not implemented, and clean up the community plugin list.

Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured):
- New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py
  (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and
  tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI /
  SLOPSMITH_PLUGINS_DIR / etc. deployments keep working.
- Fix the rename collapsing plugins/__init__.py and minigames/routes.py from
  `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant
  `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost).

Storage (app.js update-channel):
- Read feedBack-update-channel, fall back to legacy slopsmith-update-channel,
  and clear the legacy key on write — so a user's update-channel preference
  survives the rename instead of resetting to "stable".

Community plugin list (README): the rename rewrote third-party repo URLs we
don't own. Probed every one; their owners never renamed, so:
- Restore the 13 live community plugins to their real slopsmith-* names.
- Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex
  tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle).
- Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory).

Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors,
rename-sensitive + startup suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: byrongamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:03:01 +02:00

2975 lines
194 KiB
JavaScript

// Core audio graph/session capability domain host.
(function () {
'use strict';
window.feedBack = window.feedBack || {};
const capabilities = window.feedBack.capabilities;
if (!capabilities || capabilities.version !== 1) return;
if (window.feedBack.audioSession && window.feedBack.audioSession.version === 1) return;
const SCHEMA = 'feedBack.audio_session.diagnostics.v1';
const DOMAINS = Object.freeze(['audio-mix', 'audio-input', 'audio-monitoring', 'stems']);
const MAX_OUTCOMES = 100;
const MAX_DOMAIN_ITEMS = 50;
const FADER_OPERATION_TIMEOUT_MS = 2000;
const INPUT_OPERATION_TIMEOUT_MS = 2000;
const MONITORING_OPERATION_TIMEOUT_MS = 2000;
const SELECTED_INPUT_STORAGE_KEY = 'feedBack.audioInput.selectedLogicalSourceKey';
const SELECTED_MONITORING_PROVIDER_STORAGE_KEY = 'feedBack.audioMonitoring.selectedLogicalMonitoringKey';
const DIRECT_MONITOR_STORAGE_KEY = 'feedBack.audioMonitoring.directMonitorPreference';
const OWNER_ID = 'core.audio.session';
const REQUIRED_MIX_KINDS = Object.freeze(['song', 'plugin', 'stem', 'monitoring', 'preview']);
const MIX_KINDS = new Set([...REQUIRED_MIX_KINDS, 'analyser', 'other']);
const AVAILABILITY = new Set(['available', 'pending', 'unavailable', 'disabled', 'denied', 'failed', 'incompatible', 'unknown']);
const SOURCE_MODES = new Set(['native', 'compatibility', 'core']);
const INPUT_KINDS = new Set(['instrument', 'microphone', 'desktop', 'plugin', 'virtual', 'unknown']);
const CHANNEL_SHAPES = new Set(['mono', 'stereo', 'multi', 'unknown']);
const MONITORING_STATES = new Set(['active', 'degraded', 'stopped', 'unavailable', 'denied', 'failed', 'orphaned', 'unknown']);
const MONITORING_OUTCOMES = new Set(['handled', 'denied', 'degraded', 'failed', 'no-owner', 'no-handler', 'unsupported-command', 'incompatible', 'incompatible-version', 'unavailable', 'provider-selection-required', 'user-action-required', 'stopped', 'overridden']);
// Outcomes accepted from audio-mix / audio-input provider handlers. Deliberately excludes the
// monitoring-only outcomes (provider-selection-required, user-action-required, stopped, unavailable)
// so a mix/input provider can't leak a monitoring-specific terminal outcome into generic dispatch.
const PROVIDER_OUTCOMES = new Set(['handled', 'denied', 'degraded', 'failed', 'no-owner', 'no-handler', 'unsupported-command', 'incompatible', 'incompatible-version', 'overridden']);
const AUTHORIZATION_MODES = new Set(['user-action', 'attach-existing', 'background']);
const DIRECT_MONITOR_STATES = new Set(['muted', 'unmuted', 'unsupported', 'unavailable', 'unknown']);
const DIRECT_MONITOR_CONTROLS = new Set(['supported', 'unsupported', 'unavailable', 'unknown']);
// Matched against a normalized key (camelCase split to snake_case, lowercased — see _safeInputValue)
// so concatenated names like streamHandle / rawDeviceId / nativeHandleRef are caught, not just
// underscore/exact-token forms.
const LIVE_HANDLE_KEYS = /(^|_)(stream|mediastream|audionode|node|nativehandle|handle|buffer|buffers|sample|samples|waveform|rawlabel|label|device|deviceid|hardware|hardwareid|path|secret|token|password|api|apikey)(_|$)/;
// Explicitly redaction-safe label keys (normalized snake_case form) that bypass LIVE_HANDLE_KEYS
// so a provider can pass a safe display label through enumerate/_safeInputValue to _safeInputLabel.
const SAFE_INPUT_LABEL_KEYS = new Set(['safe_label', 'display_label', 'label_pseudonym', 'label_safe']);
let sequence = 0;
const knownMixParticipants = new Map();
const knownInputSources = new Map();
const knownInputProviders = new Map();
const knownMonitoringProviders = new Map();
let currentSession = _newSession({});
function _now() { return new Date().toISOString(); }
function _id(prefix) {
sequence += 1;
return `${prefix}-${sequence}`;
}
function _string(value, fallback = '') {
const normalized = String(value == null ? '' : value).trim();
return normalized || fallback;
}
function _number(value, fallback = null) {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : fallback;
}
function _bool(value, fallback = false) {
if (value === true || value === false) return value;
return fallback;
}
function _strings(value) {
return Array.isArray(value) ? value.map(item => _string(item)).filter(Boolean) : [];
}
function _plainObject(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
}
function _availability(value, fallback = 'available') {
const normalized = _string(value, fallback);
return AVAILABILITY.has(normalized) ? normalized : fallback;
}
function _mixKind(value) {
const normalized = _string(value, 'other');
return MIX_KINDS.has(normalized) ? normalized : 'other';
}
function _sourceMode(value, fallback = 'native') {
const normalized = _string(value, fallback);
return SOURCE_MODES.has(normalized) ? normalized : fallback;
}
function _inputKind(value) {
const normalized = _string(value, 'unknown');
return INPUT_KINDS.has(normalized) ? normalized : 'unknown';
}
function _channelShape(value, fallback = 'unknown') {
const normalized = _string(value, fallback);
return CHANNEL_SHAPES.has(normalized) ? normalized : fallback;
}
function _requesterId(value, fallback = 'unknown') {
// Capability dispatch does not validate `source`, so redact paths/tokens (via _boundedReason)
// before stripping to the id charset — otherwise fragments of a path/token in a malicious or
// buggy requester id would survive into redaction-safe diagnostics.
return _boundedReason(value).replace(/[^A-Za-z0-9_.:-]+/g, '-').slice(0, 80) || fallback;
}
function _redactString(value) {
// Redact absolute paths (macOS, Linux/Docker home & root, Windows). Spaces
// are allowed because song files/paths routinely contain them, but matching
// stops at line breaks, tabs, and structural delimiters so unrelated trailing
// diagnostic text on the same line is preserved rather than over-redacted.
const tail = `[^\\r\\n\\t"'\`,;(){}\\[\\]<>|]*`;
const path = new RegExp(`(?:/Users/|/home/|/root\\b/?|[A-Za-z]:\\\\)${tail}`, 'g');
return _string(value)
.replace(path, '[path]')
.replace(/\b(raw[-_ ]?audio|audio[-_ ]?buffer|sample[s]?|waveform[s]?|recording[s]?)\b/gi, '[audio-data]')
.replace(/\b(token|secret|password|api[_-]?key)=([^\s&]+)/gi, '$1=[redacted]');
}
function _boundedReason(value) {
return _redactString(value).replace(/\s+/g, ' ').slice(0, 240);
}
function _storageStatus() {
try {
if (!window.localStorage || typeof window.localStorage.getItem !== 'function') return 'unavailable';
window.localStorage.getItem(SELECTED_INPUT_STORAGE_KEY);
return 'available';
} catch (_) {
return 'failed';
}
}
function _readStorage(key) {
try {
if (!window.localStorage || typeof window.localStorage.getItem !== 'function') return '';
const stored = _string(window.localStorage.getItem(key));
// localStorage is untrusted/mutable: ignore (and clear) a persisted key that isn't already
// redaction-safe, so an injected path/token can't be restored into selected logical keys.
if (stored && key !== DIRECT_MONITOR_STORAGE_KEY && _boundedReason(stored) !== stored) {
try { window.localStorage.removeItem(key); } catch (_) { /* best effort */ }
return '';
}
return stored;
} catch (_) {
return '';
}
}
function _writeStorage(key, value) {
try {
if (!window.localStorage || typeof window.localStorage.setItem !== 'function') return 'unavailable';
window.localStorage.setItem(key, _string(value));
return 'available';
} catch (_) {
return 'failed';
}
}
function _readSelectedLogicalSourceKey() {
return _readStorage(SELECTED_INPUT_STORAGE_KEY);
}
function _writeSelectedLogicalSourceKey(logicalSourceKey) {
return _writeStorage(SELECTED_INPUT_STORAGE_KEY, logicalSourceKey);
}
function _readSelectedLogicalMonitoringKey() {
return _readStorage(SELECTED_MONITORING_PROVIDER_STORAGE_KEY);
}
function _writeSelectedLogicalMonitoringKey(logicalMonitoringKey) {
return _writeStorage(SELECTED_MONITORING_PROVIDER_STORAGE_KEY, logicalMonitoringKey);
}
function _readDirectMonitorPreference() {
const stored = _readStorage(DIRECT_MONITOR_STORAGE_KEY);
return stored === 'muted' || stored === 'unmuted' ? stored : 'muted';
}
function _writeDirectMonitorPreference(state) {
return _writeStorage(DIRECT_MONITOR_STORAGE_KEY, state === 'unmuted' ? 'unmuted' : 'muted');
}
function _storedSelectedInput() {
const logicalSourceKey = _readSelectedLogicalSourceKey();
if (!logicalSourceKey) return null;
return {
logicalSourceKey,
sourceId: '',
providerId: '',
availability: 'unavailable',
restored: true,
restoreStatus: 'missing-provider',
selectedAt: '',
lastSelectedAt: '',
lastRestoredAt: _now(),
};
}
function _storedSelectedMonitoringProvider() {
const logicalMonitoringKey = _readSelectedLogicalMonitoringKey();
if (!logicalMonitoringKey) return null;
return {
providerId: '',
logicalMonitoringKey,
availability: 'unavailable',
restored: true,
restoreStatus: 'missing-provider',
selectedAt: '',
lastSelectedAt: '',
lastRestoredAt: _now(),
requesterId: 'storage',
};
}
function _storedDirectMonitorPreference() {
return {
state: _readDirectMonitorPreference(),
control: 'unknown',
preference: _readDirectMonitorPreference(),
applied: false,
reason: '',
lastChangedAt: '',
};
}
function _clone(value) {
if (value == null || typeof value !== 'object') return value;
try { return JSON.parse(JSON.stringify(value)); }
catch (_) { return null; }
}
function _safeInputValue(value, depth = 0, seen = null) {
if (typeof value === 'string') return _boundedReason(value);
if (value == null || typeof value === 'number' || typeof value === 'boolean') return value;
if (depth > 6) return '[truncated]';
if (typeof value === 'object') {
const refs = seen || new WeakSet();
if (refs.has(value)) return '[circular]';
refs.add(value);
if (Array.isArray(value)) return value.slice(0, 20).map(item => _safeInputValue(item, depth + 1, refs));
const out = {};
for (const [key, item] of Object.entries(value).slice(0, 30)) {
// Normalize camelCase to snake_case so concatenated keys (streamHandle, rawDeviceId,
// nativeHandleRef) are caught by the segment-delimited LIVE_HANDLE_KEYS pattern.
const normalizedKey = _string(key).replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2').toLowerCase();
// Explicitly-safe label keys are allowed through (the `label` token would otherwise
// strip them) so providers can supply a redaction-safe display label via enumerate.
if (!SAFE_INPUT_LABEL_KEYS.has(normalizedKey) && LIVE_HANDLE_KEYS.test(normalizedKey)) continue;
if (typeof item === 'function') continue;
out[key] = _safeInputValue(item, depth + 1, refs);
}
return out;
}
return '';
}
function _safeInputLabel(source, fallback) {
const explicit = _string(source.safeLabel || source.displayLabel || source.labelPseudonym || source.label || source.name, fallback);
const redacted = _boundedReason(explicit).slice(0, 80);
const suspicious = redacted !== explicit || /\b(serial|hardware|device|token|secret|password|api[_-]?key)\b/i.test(redacted) || /\d{4,}/.test(redacted);
if (suspicious && source.labelSafe !== true && !source.safeLabel && !source.displayLabel && !source.labelPseudonym) return fallback;
return redacted || fallback;
}
function _newSession(options) {
const source = _plainObject(options);
return {
sessionId: _string(source.sessionId, 'main:idle'),
playerId: _string(source.playerId, 'main'),
songKey: _string(source.songKey, ''),
songFormat: _string(source.songFormat, 'unknown'),
state: _string(source.state, 'idle'),
routeState: _normalizeRoute(source.routeState || source.route || {}),
analyserState: _normalizeAnalyser(source.analyserState || source.analyser || {}),
mixParticipants: new Map(),
inputProviders: new Map(),
inputSources: new Map(),
selectedInput: _storedSelectedInput(),
openInputSessions: new Map(),
storageStatus: _storageStatus(),
monitoringProviders: new Map(),
selectedMonitoringProvider: _storedSelectedMonitoringProvider(),
directMonitor: _storedDirectMonitorPreference(),
monitoringSessions: new Map(),
stemOwner: null,
stemClaims: new Map(),
bridges: new Map(),
outcomes: [],
createdAt: _now(),
updatedAt: _now(),
};
}
function _touch() {
currentSession.updatedAt = _now();
_contributeDiagnostics();
}
function _normalizeRoute(route) {
const source = _plainObject(route);
return {
routeId: _string(source.routeId || source.id, 'route:unknown'),
routeKind: _string(source.routeKind || source.kind, 'unknown'),
availability: _string(source.availability, 'unknown'),
selectedByUser: source.selectedByUser === true,
devicePseudonym: _string(source.devicePseudonym || source.deviceId || source.deviceLabel, ''),
fallbackReason: _boundedReason(source.fallbackReason || source.reason),
lastChangedAt: _string(source.lastChangedAt, _now()),
};
}
function _normalizeAnalyser(analyser) {
const source = _plainObject(analyser);
return {
source: _string(source.source, 'unavailable'),
availability: _string(source.availability, 'unavailable'),
participantId: _string(source.participantId || source.providerId, ''),
reason: _boundedReason(source.reason),
lastChangedAt: _string(source.lastChangedAt, _now()),
};
}
function _normalizeFader(fader) {
const source = _plainObject(fader);
if (!Object.keys(source).length) return null;
const min = _number(source.min, 0);
const max = _number(source.max, 1);
const step = _number(source.step, 0.01);
const defaultValue = _clamp(_number(source.defaultValue, min), min, max > min ? max : min + 1);
const currentValue = _number(source.currentValue, _number(source.value, defaultValue));
return {
id: _string(source.faderId || source.id, 'main'),
label: _string(source.label, 'Volume'),
unit: _string(source.unit, ''),
min,
max: max > min ? max : min + 1,
step: step > 0 ? step : 0.01,
defaultValue,
currentValue: _clamp(currentValue, min, max > min ? max : min + 1),
lastRequestedValue: _number(source.lastRequestedValue, null),
lastRejectedValue: _number(source.lastRejectedValue, null),
userAdjustable: source.userAdjustable !== false && source.readOnly !== true,
availability: _availability(source.availability, 'available'),
lastCommittedAt: _string(source.lastCommittedAt, ''),
};
}
function _clamp(value, min, max) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return min;
return Math.min(max, Math.max(min, numeric));
}
function _normalizeOperationHandlers(source) {
const handlers = {};
const operationHandlers = _plainObject(source.operationHandlers || source.handlers || source.providerOperations);
for (const [key, value] of Object.entries(operationHandlers)) {
if (typeof value === 'function') handlers[key] = value;
}
if (typeof source.getValue === 'function') handlers['fader.get-value'] = source.getValue;
if (typeof source.setValue === 'function') handlers['fader.set-value'] = source.setValue;
const fader = _plainObject(source.fader);
if (typeof fader.getValue === 'function') handlers['fader.get-value'] = fader.getValue;
if (typeof fader.setValue === 'function') handlers['fader.set-value'] = fader.setValue;
return handlers;
}
function _logicalFaderKey(participant, source) {
const explicit = source.logicalFaderKey || source.logicalKey || (source.fader && source.fader.logicalFaderKey);
if (explicit) return _string(explicit);
const faderId = participant.fader ? participant.fader.id : 'main';
const owner = participant.ownerPluginId || participant.participantId;
return `${owner}:${faderId}`;
}
function _normalizeParticipant(spec) {
const source = _plainObject(spec);
const participantId = _string(source.participantId || source.id);
if (!participantId) return null;
const compatibilitySource = _string(source.compatibilitySource || source.legacySurface, '');
const sourceMode = _sourceMode(source.sourceMode, source.ownerPluginId === 'core' || source.pluginId === 'core' || source.source === 'core' ? 'core' : (compatibilitySource ? 'compatibility' : 'native'));
return {
participantId,
ownerPluginId: _string(source.ownerPluginId || source.pluginId || source.source, 'core'),
label: _string(source.label || source.name, participantId),
kind: _mixKind(source.kind),
fader: _normalizeFader(source.fader),
operations: _strings(source.operations || source.providerOperations),
operationHandlers: _normalizeOperationHandlers(source),
availability: _availability(source.availability, source.disabled ? 'disabled' : (currentSession.state === 'active' ? 'available' : 'pending')),
sourceMode,
compatibilitySource,
supersededBy: _string(source.supersededBy, ''),
registeredAt: _string(source.registeredAt, _now()),
lastSeenAt: _now(),
};
}
function _summaryParticipant(participant) {
const clone = _clone(participant);
if (clone) delete clone.operationHandlers;
return clone;
}
function _summaryFader(participant, options = {}) {
if (!participant || !participant.fader) return null;
const fader = participant.fader;
const availability = options.availability || _effectiveFaderAvailability(participant);
return {
participantId: participant.participantId,
ownerPluginId: participant.ownerPluginId,
label: participant.label,
kind: participant.kind,
sourceMode: participant.sourceMode,
compatibilitySource: participant.compatibilitySource,
supersededBy: participant.supersededBy || '',
logicalFaderKey: participant.logicalFaderKey,
faderId: fader.id,
id: fader.id,
faderKey: _faderKey(participant.participantId, fader.id),
faderLabel: fader.label,
unit: fader.unit,
min: fader.min,
max: fader.max,
step: fader.step,
defaultValue: fader.defaultValue,
currentValue: fader.currentValue,
lastRequestedValue: fader.lastRequestedValue,
lastRejectedValue: fader.lastRejectedValue,
userAdjustable: fader.userAdjustable && availability === 'available',
availability,
lastCommittedAt: fader.lastCommittedAt,
};
}
function _normalizeSource(spec) {
const source = _plainObject(spec);
const sourceId = _string(source.sourceId || source.id);
const providerId = _string(source.providerId || source.participantId);
const logicalSourceKey = _string(source.logicalSourceKey || source.logicalKey || source.sourceKey);
if (!sourceId || !providerId || !logicalSourceKey) return null;
const channelSummary = _normalizeChannelSummary(source.channelSummary || source.channel || source);
const now = _now();
const compatibilitySource = _string(source.compatibilitySource || source.legacySurface, '');
const sourceMode = _sourceMode(source.sourceMode, compatibilitySource ? 'compatibility' : 'native');
return {
sourceId,
logicalSourceKey,
providerId,
ownerPluginId: _string(source.ownerPluginId || source.pluginId || providerId, providerId),
kind: _inputKind(source.kind),
label: _safeInputLabel(source, _string(source.diagnosticsPseudonym || source.pseudonym, 'Input source')),
channelSummary,
channelCount: channelSummary.channelCount,
availability: _availability(source.availability, source.disabled ? 'disabled' : 'available'),
selected: source.selected === true,
sourceMode,
compatibilitySource,
supersededBy: _string(source.supersededBy, ''),
reason: _boundedReason(source.reason || source.failureReason || source.unavailableReason),
operations: _strings(source.operations || source.providerOperations),
diagnosticsPseudonym: _string(source.diagnosticsPseudonym || source.pseudonym || sourceId, sourceId),
registeredAt: _string(source.registeredAt, now),
lastSeenAt: now,
lastChangedAt: _string(source.lastChangedAt, now),
};
}
function _normalizeChannelSummary(value) {
const source = _plainObject(value);
const count = _number(source.channelCount ?? source.channels, null);
const inferred = count === 1 ? 'mono' : (count === 2 ? 'stereo' : (count && count > 2 ? 'multi' : 'unknown'));
const shape = _channelShape(source.channelShape || source.shape, inferred);
const supports = _strings(source.supports || source.supportedShapes).map(item => _channelShape(item, '')).filter(Boolean);
return {
channelCount: count,
channelShape: shape,
supports: supports.length ? Array.from(new Set(supports)) : (shape !== 'unknown' ? [shape] : []),
defaultShape: _channelShape(source.defaultShape, shape),
reason: _boundedReason(source.reason),
};
}
function _normalizeInputProvider(source, input) {
const providerId = input.providerId;
const existing = currentSession.inputProviders.get(providerId) || knownInputProviders.get(providerId) || {};
const operationHandlers = { ...(existing.operationHandlers || {}), ..._normalizeOperationHandlers(source) };
const operations = Array.from(new Set([...(existing.operations || []), ...input.operations, ...Object.keys(operationHandlers)]));
const provider = {
providerId,
ownerPluginId: _string(source.ownerPluginId || source.pluginId || existing.ownerPluginId || input.ownerPluginId, input.ownerPluginId),
label: _safeInputLabel(source, _string(existing.label || providerId, providerId)),
sourceMode: input.sourceMode,
operations,
operationHandlers,
availability: _availability(source.providerAvailability || source.availability || existing.availability, 'available'),
version: _number(source.version || existing.version, 1),
registeredAt: existing.registeredAt || _now(),
lastSeenAt: _now(),
};
knownInputProviders.set(providerId, provider);
currentSession.inputProviders.set(providerId, provider);
return provider;
}
function _inputSourcePriority(source) {
if (source.sourceMode === 'core') return 0;
if (source.sourceMode === 'native') return 1;
return 2;
}
function _inputSourceAvailabilityRank(source) {
return source.availability === 'available' ? 0 : (source.availability === 'pending' ? 1 : 2);
}
function _sourceRecordKey(source) {
return `${source.providerId}:${source.sourceMode}:${source.logicalSourceKey}`;
}
function _findInputSource(query = {}) {
const source = _plainObject(query);
const sourceId = _string(source.sourceId || source.id);
const providerId = _string(source.providerId);
const logicalSourceKey = _string(source.logicalSourceKey || source.logicalKey || source.sourceKey);
if (sourceId && currentSession.inputSources.has(sourceId)) return currentSession.inputSources.get(sourceId);
let matches = Array.from(currentSession.inputSources.values()).filter(item => item.logicalSourceKey === logicalSourceKey);
// When a providerId is supplied (e.g. unregister-source), prefer that provider's source so a
// native + compatibility duplicate sharing one logical key resolves to the intended one.
if (providerId) {
const byProvider = matches.filter(item => item.providerId === providerId);
if (byProvider.length) matches = byProvider;
}
if (!matches.length) return null;
matches.sort((a, b) => _inputSourcePriority(a) - _inputSourcePriority(b) || _inputSourceAvailabilityRank(a) - _inputSourceAvailabilityRank(b) || a.providerId.localeCompare(b.providerId) || a.sourceId.localeCompare(b.sourceId));
return matches[0];
}
function _visibleInputSources() {
_refreshInputDuplicateSuppression();
return Array.from(currentSession.inputSources.values())
.filter(source => !source.supersededBy)
.sort((a, b) => _inputSourcePriority(a) - _inputSourcePriority(b) || a.label.localeCompare(b.label) || a.providerId.localeCompare(b.providerId));
}
function _refreshInputDuplicateSuppression() {
const groups = new Map();
for (const source of currentSession.inputSources.values()) {
source.supersededBy = '';
const list = groups.get(source.logicalSourceKey) || [];
list.push(source);
groups.set(source.logicalSourceKey, list);
}
for (const list of groups.values()) {
if (list.length < 2) continue;
list.sort((a, b) => _inputSourcePriority(a) - _inputSourcePriority(b) || _inputSourceAvailabilityRank(a) - _inputSourceAvailabilityRank(b) || a.providerId.localeCompare(b.providerId) || a.sourceId.localeCompare(b.sourceId));
const winner = list[0];
for (const loser of list.slice(1)) {
loser.supersededBy = winner.sourceId;
}
}
_syncSelectedInput();
}
function _recordInputDuplicateSuppression() {
for (const source of currentSession.inputSources.values()) {
if (!source.supersededBy || source.sourceMode !== 'compatibility') continue;
recordBridgeHit({
domain: 'audio-input',
bridgeId: 'audio-input.legacy-source',
legacySurface: source.compatibilitySource || 'plugin/browser input source handoff',
participantId: source.providerId,
logicalSourceKey: source.logicalSourceKey,
outcome: 'overridden',
status: 'overshadowed',
reason: `Native source owns logical source ${source.logicalSourceKey}`,
});
}
}
function _monitoringState(value, fallback = 'unknown') {
const normalized = _string(value, fallback);
if (MONITORING_STATES.has(normalized)) return normalized;
// Callers sometimes pass a non-state outcome (e.g. 'no-handler') as the fallback; clamp it
// so a session.state is always a declared MONITORING_STATES member for inspectors/events.
return MONITORING_STATES.has(fallback) ? fallback : 'unknown';
}
function _monitoringOutcome(value, fallback = 'handled') {
const normalized = _string(value, fallback);
return MONITORING_OUTCOMES.has(normalized) ? normalized : fallback;
}
function _authorizationMode(value) {
const normalized = _string(value, 'background');
return AUTHORIZATION_MODES.has(normalized) ? normalized : 'background';
}
function _directMonitorState(value, fallback = 'unknown') {
const normalized = _string(value, fallback);
return DIRECT_MONITOR_STATES.has(normalized) ? normalized : fallback;
}
function _directMonitorControl(value, fallback = 'unknown') {
const normalized = _string(value, fallback);
return DIRECT_MONITOR_CONTROLS.has(normalized) ? normalized : fallback;
}
function _normalizeDirectMonitor(value = {}) {
const source = _plainObject(value);
const preference = source.preference === 'muted' || source.preference === 'unmuted' ? source.preference : _readDirectMonitorPreference();
return {
state: _directMonitorState(source.state, preference || 'unknown'),
control: _directMonitorControl(source.control, source.supported === false ? 'unsupported' : 'unknown'),
preference,
applied: source.applied === true,
reason: _boundedReason(source.reason),
lastChangedAt: _string(source.lastChangedAt, source.changedAt || ''),
};
}
function _normalizeLatencySummary(value = {}) {
const source = _plainObject(value);
if (!Object.keys(source).length) return null;
return {
// bucket/level is provider-supplied and surfaced in diagnostics, so bound it like reason.
bucket: _boundedReason(source.bucket || source.level),
ms: _number(source.ms ?? source.latencyMs, null),
reason: _boundedReason(source.reason),
};
}
function _logicalMonitoringKey(provider, source) {
const explicit = source.logicalMonitoringKey || source.logicalKey || source.monitoringKey;
if (explicit) return _string(explicit);
return `${provider.sourceMode}:${provider.providerId}`;
}
function _normalizeMonitoringProvider(spec) {
const source = _plainObject(spec);
// providerId is surfaced in diagnostics/events and used for selection, so normalize it the same
// way requester ids are (redact paths/tokens, restrict to the id charset, bound length) before it
// becomes session state — a buggy/malicious provider must not leak a path/token through it.
const providerId = _boundedReason(_string(source.providerId || source.participantId || source.id)).replace(/[^A-Za-z0-9_.:-]+/g, '-').slice(0, 80);
if (!providerId) return null;
const existing = currentSession.monitoringProviders.get(providerId) || knownMonitoringProviders.get(providerId) || {};
const compatibilitySource = _string(source.compatibilitySource || source.legacySurface || existing.compatibilitySource, '');
const sourceMode = _sourceMode(source.sourceMode, compatibilitySource ? 'compatibility' : (source.ownerPluginId === 'core' || source.pluginId === 'core' || source.source === 'core' ? 'core' : 'native'));
const operationHandlers = { ...(existing.operationHandlers || {}), ..._normalizeOperationHandlers(source) };
const operations = Array.from(new Set([...(existing.operations || []), ..._strings(source.operations || source.providerOperations), ...Object.keys(operationHandlers)]));
const provider = {
providerId,
ownerPluginId: _string(source.ownerPluginId || source.pluginId || existing.ownerPluginId || providerId, providerId),
label: _safeInputLabel(source, _string(existing.label || providerId, providerId)),
sourceMode,
compatibilitySource,
logicalMonitoringKey: _string(source.logicalMonitoringKey || source.logicalKey || source.monitoringKey || existing.logicalMonitoringKey, ''),
availability: _availability(source.providerAvailability || source.availability || existing.availability, source.disabled ? 'disabled' : 'available'),
operations,
operationHandlers,
version: _number(source.version || existing.version, 1),
directMonitor: _normalizeDirectMonitor(source.directMonitor || existing.directMonitor || {}),
latencySummary: _normalizeLatencySummary(source.latencySummary || existing.latencySummary || {}),
reason: _boundedReason(source.reason || source.failureReason || source.unavailableReason || existing.reason),
supersededBy: _string(source.supersededBy || existing.supersededBy, ''),
registeredAt: existing.registeredAt || _string(source.registeredAt, _now()),
lastSeenAt: _now(),
lastChangedAt: _string(source.lastChangedAt || existing.lastChangedAt, _now()),
};
provider.logicalMonitoringKey = _logicalMonitoringKey(provider, source);
return provider;
}
function _monitoringProviderPriority(provider) {
if (provider.sourceMode === 'core') return 0;
if (provider.sourceMode === 'native') return 1;
return 2;
}
function _findMonitoringProvider(query = {}) {
const source = _plainObject(query);
const providerId = _string(source.providerId || source.participantId || source.id);
const logicalMonitoringKey = _string(source.logicalMonitoringKey || source.logicalKey || source.monitoringKey);
if (providerId && currentSession.monitoringProviders.has(providerId)) return currentSession.monitoringProviders.get(providerId);
const matches = Array.from(currentSession.monitoringProviders.values()).filter(item => item.logicalMonitoringKey === logicalMonitoringKey);
if (!matches.length) return null;
matches.sort((a, b) => _monitoringProviderPriority(a) - _monitoringProviderPriority(b) || _inputSourceAvailabilityRank(a) - _inputSourceAvailabilityRank(b) || a.providerId.localeCompare(b.providerId));
return matches[0];
}
function _refreshMonitoringDuplicateSuppression() {
const groups = new Map();
for (const provider of currentSession.monitoringProviders.values()) {
provider.supersededBy = '';
const list = groups.get(provider.logicalMonitoringKey) || [];
list.push(provider);
groups.set(provider.logicalMonitoringKey, list);
}
for (const list of groups.values()) {
if (list.length < 2) continue;
list.sort((a, b) => _monitoringProviderPriority(a) - _monitoringProviderPriority(b) || _inputSourceAvailabilityRank(a) - _inputSourceAvailabilityRank(b) || a.providerId.localeCompare(b.providerId));
const winner = list[0];
for (const loser of list.slice(1)) loser.supersededBy = winner.providerId;
}
_syncSelectedMonitoringProvider();
}
function _recordMonitoringDuplicateSuppression() {
for (const provider of currentSession.monitoringProviders.values()) {
if (!provider.supersededBy || provider.sourceMode !== 'compatibility') continue;
recordBridgeHit({
domain: 'audio-monitoring',
bridgeId: provider.compatibilitySource || 'audio-monitoring.legacy-provider',
legacySurface: provider.compatibilitySource || 'plugin/browser monitoring handoff',
participantId: provider.providerId,
outcome: 'overridden',
status: 'overshadowed',
reason: `Native monitoring provider ${provider.supersededBy} owns logical path ${provider.logicalMonitoringKey}`,
});
}
}
function _visibleMonitoringProviders() {
_refreshMonitoringDuplicateSuppression();
return Array.from(currentSession.monitoringProviders.values())
.filter(provider => !provider.supersededBy)
.sort((a, b) => _monitoringProviderPriority(a) - _monitoringProviderPriority(b) || a.label.localeCompare(b.label) || a.providerId.localeCompare(b.providerId));
}
function _syncSelectedMonitoringProvider() {
const selected = currentSession.selectedMonitoringProvider;
if (!selected || !selected.logicalMonitoringKey) return null;
const provider = _findMonitoringProvider({ logicalMonitoringKey: selected.logicalMonitoringKey });
if (provider && !provider.supersededBy) {
selected.providerId = provider.providerId;
selected.availability = provider.availability;
selected.restoreStatus = provider.availability === 'available' ? (selected.restored ? 'restored' : 'available') : provider.availability;
if (selected.restored && !selected.lastRestoredAt) selected.lastRestoredAt = _now();
} else {
selected.providerId = '';
selected.availability = 'unavailable';
selected.restoreStatus = selected.restored ? 'missing-provider' : 'unavailable';
}
return selected;
}
function _makeSelectedMonitoringProvider(provider, requester) {
const now = _now();
return {
providerId: provider.providerId,
logicalMonitoringKey: provider.logicalMonitoringKey,
availability: provider.availability,
restored: false,
restoreStatus: provider.availability === 'available' ? 'available' : provider.availability,
selectedAt: now,
lastSelectedAt: now,
lastRestoredAt: '',
requesterId: _requesterId(requester),
};
}
function _summaryMonitoringProvider(provider) {
if (!provider) return null;
return {
providerId: provider.providerId,
ownerPluginId: provider.ownerPluginId,
label: provider.label,
sourceMode: provider.sourceMode,
compatibilitySource: provider.compatibilitySource || '',
logicalMonitoringKey: provider.logicalMonitoringKey,
availability: provider.availability,
operations: _strings(provider.operations),
directMonitor: _clone(provider.directMonitor),
latencySummary: _clone(provider.latencySummary),
reason: provider.reason || '',
supersededBy: provider.supersededBy || '',
registeredAt: provider.registeredAt || '',
lastSeenAt: provider.lastSeenAt || '',
lastChangedAt: provider.lastChangedAt || '',
};
}
function _summarySelectedMonitoringProvider(selected) {
if (!selected) return null;
return {
providerId: selected.providerId || '',
logicalMonitoringKey: selected.logicalMonitoringKey || '',
availability: selected.availability || 'unavailable',
restored: !!selected.restored,
restoreStatus: selected.restoreStatus || 'not-selected',
selectedAt: selected.selectedAt || selected.lastSelectedAt || '',
lastSelectedAt: selected.lastSelectedAt || '',
lastRestoredAt: selected.lastRestoredAt || '',
requesterId: selected.requesterId || '',
};
}
function _monitoringSourceRef(openResult, fallback = {}) {
const payload = _plainObject(openResult && openResult.payload ? openResult.payload : openResult);
const source = _plainObject(fallback);
return {
logicalSourceKey: _string(payload.logicalSourceKey || source.logicalSourceKey, ''),
sourceId: _string(payload.sourceId || source.sourceId, ''),
providerId: _string(payload.providerId || source.providerId, ''),
availability: _string(payload.state || payload.availability || source.availability, ''),
channelShape: _channelShape(payload.channelShape || source.channelShape, 'unknown'),
openSessionId: _string(payload.openSessionId || payload.openInputSessionId || source.openSessionId, ''),
};
}
function _monitoringSessionKey(provider, sourceRef, requiredChannelShape, directMonitorPolicy) {
return [
provider && provider.logicalMonitoringKey,
sourceRef && sourceRef.logicalSourceKey,
_channelShape(requiredChannelShape, 'unknown'),
directMonitorPolicy || 'default',
].map(item => _string(item, 'unknown')).join('::');
}
function _findCompatibleMonitoringSession(provider, sourceRef, requiredChannelShape, directMonitorPolicy) {
const key = _monitoringSessionKey(provider, sourceRef, requiredChannelShape, directMonitorPolicy);
for (const session of currentSession.monitoringSessions.values()) {
if (session.sessionKey === key && (session.state === 'active' || session.state === 'degraded')) return session;
}
return null;
}
function _monitoringRequesterRef(source = {}) {
return {
requesterId: _requesterId(source.requesterId || source.requester || source.source, 'unknown'),
purpose: _boundedReason(source.purpose).slice(0, 80),
requiredChannelShape: _channelShape(source.requiredChannelShape, 'unknown'),
directMonitorRequirement: _directMonitorState(source.directMonitorRequirement, source.directMonitorRequirement ? 'unknown' : 'unknown'),
status: _string(source.status, ''),
reason: _boundedReason(source.reason),
attachedAt: _string(source.attachedAt, _now()),
};
}
function _withMonitoringTimeout(promise, operation) {
let timer = null;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => {
const err = new Error(`${operation} timed out after ${MONITORING_OPERATION_TIMEOUT_MS}ms`);
err.timedOut = true;
reject(err);
}, MONITORING_OPERATION_TIMEOUT_MS);
});
return Promise.race([Promise.resolve(promise), timeout]).finally(() => { if (timer !== null) clearTimeout(timer); });
}
function _monitoringProviderOutcome(raw, fallbackPayload = {}, voidIsFailure = false) {
// Per the control-plane contract a malformed (non-object) provider response is a failure, not a
// silently-'handled' active session. A void return (null/undefined) is lenient for stop/status
// (some handlers legitimately return nothing) but a failure for monitoring.start (voidIsFailure),
// which must not report monitoring active when the provider returned no result.
const isVoid = raw === null || raw === undefined;
if ((isVoid && voidIsFailure) || (!isVoid && (typeof raw !== 'object' || Array.isArray(raw)))) {
return { outcome: 'failed', status: 'failed', reason: _boundedReason(isVoid ? 'Monitoring provider returned no result' : 'Malformed monitoring provider response'), payload: fallbackPayload };
}
if (isVoid) {
// Void return tolerated for stop/status: treat as handled but with a neutral 'unknown' status
// (never 'active', which would fake an active session) and surface fallbackPayload verbatim.
return { outcome: 'handled', status: 'unknown', reason: '', payload: fallbackPayload };
}
const source = _plainObject(raw);
const outcome = _monitoringOutcome(source.outcome, 'handled');
const payload = _safeInputValue(source.payload || source.session || source.summary || source, 0) || fallbackPayload;
return {
outcome,
status: _monitoringState(source.status || source.state, outcome === 'handled' ? 'active' : outcome),
reason: _boundedReason(source.reason || source.error || source.message),
payload,
};
}
function _monitoringEventFor(outcome, state) {
if (outcome === 'handled' && state === 'active') return 'monitoring-started';
if (outcome === 'degraded' || state === 'degraded') return 'monitoring-degraded';
if (outcome === 'denied' || state === 'denied') return 'monitoring-denied';
if (outcome === 'failed' || state === 'failed' || outcome === 'incompatible' || outcome === 'incompatible-version') return 'monitoring-failed';
if (outcome === 'stopped' || state === 'stopped') return 'monitoring-stopped';
if (outcome === 'provider-selection-required') return 'provider-selection-required';
return 'monitoring-unavailable';
}
function _emitMonitoringOutcome(outcome, session, extra = {}) {
// Events are broadcast to all observers, so emit the redaction-safe session shape (same as
// diagnostics) rather than a raw clone that would leak internal sessionKey/monitoringId/sourceRef.
const payload = { ..._redactedMonitoringSession(session, _newPseudonymizer()), ...extra };
const event = _monitoringEventFor(outcome, session && session.state);
capabilities.emitEvent('audio-monitoring', event, payload);
if (outcome === 'denied' || (session && session.state === 'denied')) capabilities.emitEvent('audio-monitoring', 'permission-denied', payload);
}
function _monitoringResultFor(outcome, reason, payload) {
if (outcome === 'handled') return _handled(payload);
if (outcome === 'denied') return _denied(reason, payload);
if (outcome === 'degraded') return _degraded(reason, payload);
if (outcome === 'failed') return _failed(reason, payload);
if (outcome === 'no-owner') return _noOwner(reason, payload);
if (outcome === 'no-handler') return _noHandler(reason, payload);
if (outcome === 'unsupported-command') return _unsupportedCommand(reason, payload);
if (outcome === 'incompatible') return _incompatible(reason, payload);
if (outcome === 'incompatible-version') return _incompatibleVersion(reason, payload);
if (outcome === 'provider-selection-required') return _providerSelectionRequired(reason, payload);
if (outcome === 'user-action-required') return _userActionRequired(reason, payload);
if (outcome === 'stopped') return _stopped(reason, payload);
if (outcome === 'unavailable') return _unavailable(reason, payload);
if (outcome === 'overridden') return { outcome: 'overridden', reason, payload };
return _degraded(reason, payload);
}
function _syncSelectedInput() {
const selected = currentSession.selectedInput;
if (!selected || !selected.logicalSourceKey) {
for (const source of currentSession.inputSources.values()) source.selected = false;
return null;
}
const source = _findInputSource({ logicalSourceKey: selected.logicalSourceKey });
for (const item of currentSession.inputSources.values()) item.selected = item.logicalSourceKey === selected.logicalSourceKey && item.sourceId === (source && source.sourceId);
if (source) {
selected.sourceId = source.sourceId;
selected.providerId = source.providerId;
selected.availability = source.availability;
selected.restoreStatus = source.availability === 'available' ? (selected.restored ? 'restored' : 'available') : source.availability;
if (selected.restored && !selected.lastRestoredAt) selected.lastRestoredAt = _now();
} else {
selected.sourceId = '';
selected.providerId = '';
selected.availability = 'unavailable';
selected.restoreStatus = selected.restored ? 'missing-provider' : 'unavailable';
}
return selected;
}
function _makeSelectedInput(source, requester) {
const now = _now();
return {
logicalSourceKey: source.logicalSourceKey,
sourceId: source.sourceId,
providerId: source.providerId,
availability: source.availability,
restored: false,
restoreStatus: source.availability === 'available' ? 'available' : source.availability,
selectedAt: now,
lastSelectedAt: now,
lastRestoredAt: '',
requesterId: _requesterId(requester),
};
}
function _requiredChannelShape(value) {
const source = _plainObject(value);
return _channelShape(source.requiredChannelShape || source.channelShape || source.shape || value, 'unknown');
}
function _channelCompatible(channelSummary, requiredChannelShape) {
const required = _channelShape(requiredChannelShape, 'unknown');
if (required === 'unknown') return { ok: true, channelShape: _channelShape(channelSummary && channelSummary.defaultShape, _channelShape(channelSummary && channelSummary.channelShape, 'unknown')) };
const summary = _plainObject(channelSummary);
const supports = Array.isArray(summary.supports) ? summary.supports.map(item => _channelShape(item, '')).filter(Boolean) : [];
const shape = _channelShape(summary.channelShape, 'unknown');
const count = _number(summary.channelCount, null);
const compatible = supports.includes(required) || shape === required || (required === 'mono' && count && count >= 1) || (required === 'stereo' && count && count >= 2) || (required === 'multi' && count && count > 2);
return { ok: compatible, channelShape: required };
}
function _openSessionKey(logicalSourceKey, channelShape) {
return `${_string(logicalSourceKey)}::${_channelShape(channelShape, 'unknown')}`;
}
function _requesterRef(requesterId, purpose) {
return { requesterId: _requesterId(requesterId), purpose: _boundedReason(purpose).slice(0, 80), openedAt: _now() };
}
function _withInputTimeout(promise, operation) {
let timer = null;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => {
const err = new Error(`${operation} timed out after ${INPUT_OPERATION_TIMEOUT_MS}ms`);
err.timedOut = true;
reject(err);
}, INPUT_OPERATION_TIMEOUT_MS);
});
return Promise.race([Promise.resolve(promise), timeout]).finally(() => { if (timer !== null) clearTimeout(timer); });
}
function _unsupportedCommand(reason, payload) { return { outcome: 'unsupported-command', reason, payload }; }
function _providerOutcome(raw, fallbackPayload = {}) {
const source = _plainObject(raw);
const outcome = _string(source.outcome, 'handled');
const payload = _safeInputValue(source.payload || source.session || source.summary || source, 0) || fallbackPayload;
return {
outcome: PROVIDER_OUTCOMES.has(outcome) ? outcome : 'handled',
status: _string(source.status || source.state, ''),
reason: _boundedReason(source.reason || source.error || source.message),
payload,
};
}
function _recordOutcome(entry) {
const source = _plainObject(entry);
const outcome = _string(source.outcome, 'handled');
const record = {
domain: DOMAINS.includes(source.domain) ? source.domain : 'audio-mix',
operation: _string(source.operation || source.command || source.event, 'inspect'),
participantId: _string(source.participantId || source.ownerId || source.requester, ''),
providerId: _string(source.providerId, ''),
monitoringId: _string(source.monitoringId, ''),
sourceId: _string(source.sourceId, ''),
logicalSourceKey: _string(source.logicalSourceKey, ''),
requesterId: _string(source.requesterId, ''),
openSessionId: _string(source.openSessionId, ''),
faderId: _string(source.faderId, ''),
bridgeId: _string(source.bridgeId, ''),
outcome,
status: _string(source.status, ''),
reason: _boundedReason(source.reason),
timestamp: _string(source.timestamp, _now()),
};
currentSession.outcomes.push(record);
while (currentSession.outcomes.length > MAX_OUTCOMES) currentSession.outcomes.shift();
currentSession.updatedAt = _now();
return record;
}
function _redactedOutcome(outcome, pseudonymize) {
const clone = _clone(outcome) || {};
if (clone.domain === 'audio-input' || clone.domain === 'audio-monitoring') {
if (clone.sourceId) clone.sourceId = pseudonymize(clone.sourceId, 'source');
// openSessionId is left verbatim: it is an internally-generated `input-open-<n>` id (not
// sensitive) and must correlate with openSessions[].openSessionId in the same snapshot.
// The one untrusted entry point (a close-source miss) is bounded at its call site instead.
// logicalSourceKey/providerId/participantId/requesterId are only *supposed* to be
// redaction-safe; some paths derive them from caller-supplied values (e.g. an enumerate
// option payload), so bound them so a path/token cannot leak into diagnostics.
if (clone.logicalSourceKey) clone.logicalSourceKey = _boundedReason(clone.logicalSourceKey);
if (clone.providerId) clone.providerId = _boundedReason(clone.providerId);
if (clone.participantId) clone.participantId = _boundedReason(clone.participantId);
if (clone.requesterId) clone.requesterId = _boundedReason(clone.requesterId);
}
if (clone.domain === 'audio-monitoring' && clone.monitoringId) clone.monitoringId = pseudonymize(clone.monitoringId, 'monitoring');
// audio-input openSessionId is always an internally-generated `input-open-<n>` id (the one
// untrusted close-source path bounds it at its call site), so it stays verbatim to correlate
// with openSessions[]. On audio-monitoring it can be caller-supplied via sourceRef.openSessionId,
// so only keep it verbatim when it names a live internally-tracked open-input session (preserving
// the cross-domain correlation); pseudonymize anything else — a raw/native handle, or a spoofed
// id matching the safe format that would otherwise falsely correlate to another session.
if (clone.domain === 'audio-monitoring' && clone.openSessionId
&& !Array.from(currentSession.openInputSessions.values()).some(item => item.openSessionId === clone.openSessionId)) {
clone.openSessionId = pseudonymize(clone.openSessionId, 'input-open');
}
if (clone.reason) clone.reason = _boundedReason(clone.reason);
return clone;
}
function _handled(payload) { return { outcome: 'handled', payload }; }
function _degraded(reason, payload) { return { outcome: 'degraded', reason, payload }; }
function _failed(reason, payload) { return { outcome: 'failed', reason, payload }; }
function _noHandler(reason, payload) { return { outcome: 'no-handler', reason, payload }; }
function _noOwner(reason, payload) { return { outcome: 'no-owner', reason, payload }; }
function _incompatible(reason, payload) { return { outcome: 'incompatible', reason, payload }; }
function _incompatibleVersion(reason, payload) { return { outcome: 'incompatible-version', reason, payload }; }
function _unavailable(reason, payload) { return { outcome: 'unavailable', reason, payload }; }
function _providerSelectionRequired(reason, payload) { return { outcome: 'provider-selection-required', reason, payload }; }
function _userActionRequired(reason, payload) { return { outcome: 'user-action-required', reason, payload }; }
function _stopped(reason, payload) { return { outcome: 'stopped', reason, payload }; }
function _denied(reason, payload) { return { outcome: 'denied', reason, payload }; }
function _faderKey(participantId, faderId) {
return `${_string(participantId)}:${_string(faderId, 'main')}`;
}
function _findParticipant(participantId, faderId) {
const id = _string(participantId);
if (id && currentSession.mixParticipants.has(id)) return currentSession.mixParticipants.get(id);
const requestedFaderId = _string(faderId);
for (const participant of currentSession.mixParticipants.values()) {
if (participant.fader && participant.fader.id === requestedFaderId) return participant;
}
return null;
}
function _effectiveFaderAvailability(participant) {
if (!participant || !participant.fader) return 'unavailable';
if (participant.supersededBy) return 'disabled';
if (participant.availability !== 'available') return participant.availability;
if (participant.fader.availability !== 'available') return participant.fader.availability;
if (!participant.fader.userAdjustable) return 'disabled';
if (currentSession.state !== 'active' && participant.kind !== 'song') return 'pending';
return 'available';
}
function _operationPriority(participant) {
if (participant.sourceMode === 'core') return 0;
if (participant.sourceMode === 'native') return 1;
return 2;
}
function _refreshDuplicateSuppression() {
const groups = new Map();
for (const participant of currentSession.mixParticipants.values()) {
participant.supersededBy = '';
if (!participant.fader) continue;
const list = groups.get(participant.logicalFaderKey) || [];
list.push(participant);
groups.set(participant.logicalFaderKey, list);
}
for (const list of groups.values()) {
if (list.length < 2) continue;
list.sort((a, b) => _operationPriority(a) - _operationPriority(b) || a.participantId.localeCompare(b.participantId));
const winner = list[0];
for (const loser of list.slice(1)) {
loser.supersededBy = winner.participantId;
}
}
}
function _recordDuplicateSuppression() {
for (const participant of currentSession.mixParticipants.values()) {
if (!participant.supersededBy || participant.sourceMode !== 'compatibility') continue;
recordBridgeHit({
domain: 'audio-mix',
bridgeId: participant.compatibilitySource || 'audio-mix.fader-registry',
legacySurface: participant.compatibilitySource || 'registerFader',
participantId: participant.participantId,
outcome: 'overridden',
status: 'overshadowed',
reason: `Native audio-mix participant ${participant.supersededBy} owns logical fader ${participant.logicalFaderKey}`,
});
}
}
function _visibleFaderParticipants() {
_refreshDuplicateSuppression();
return Array.from(currentSession.mixParticipants.values())
.filter(participant => participant.fader && !participant.supersededBy)
.sort((a, b) => REQUIRED_MIX_KINDS.indexOf(a.kind) - REQUIRED_MIX_KINDS.indexOf(b.kind) || a.label.localeCompare(b.label) || a.participantId.localeCompare(b.participantId));
}
function _withFaderTimeout(promise, participant, operation) {
let timer = null;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => {
const err = new Error(`${operation} timed out after ${FADER_OPERATION_TIMEOUT_MS}ms`);
err.timedOut = true;
reject(err);
}, FADER_OPERATION_TIMEOUT_MS);
});
return Promise.race([Promise.resolve(promise), timeout]).finally(() => { if (timer !== null) clearTimeout(timer); });
}
function _extractCommittedValue(result, fallback) {
if (Number.isFinite(Number(result))) return Number(result);
const source = _plainObject(result);
const value = _number(source.committedValue, _number(source.currentValue, _number(source.value, fallback)));
return Number.isFinite(value) ? value : fallback;
}
function _emitFaderUnavailable(participant, reason) {
const payload = _summaryFader(participant, { availability: _effectiveFaderAvailability(participant) });
if (payload) payload.reason = _boundedReason(reason);
capabilities.emitEvent('audio-mix', 'fader-unavailable', payload || { participantId: participant && participant.participantId, reason: _boundedReason(reason) });
}
async function listFaders() {
const faders = _visibleFaderParticipants().map(participant => _summaryFader(participant));
const kinds = Object.fromEntries(REQUIRED_MIX_KINDS.map(kind => [kind, faders.some(fader => fader.kind === kind)]));
_recordOutcome({ domain: 'audio-mix', operation: 'list-faders', participantId: OWNER_ID, outcome: 'handled', status: `${faders.length}` });
return _handled({ faders, requiredKinds: kinds, timeoutMs: FADER_OPERATION_TIMEOUT_MS });
}
async function getFaderValue(payload = {}) {
const participant = _findParticipant(payload.participantId, payload.faderId || payload.id);
if (!participant || !participant.fader) {
const result = _noHandler('Unknown audio-mix fader', { participantId: payload.participantId || '', faderId: payload.faderId || payload.id || '' });
_recordOutcome({ domain: 'audio-mix', operation: 'get-fader-value', participantId: result.payload.participantId, faderId: result.payload.faderId, outcome: result.outcome, reason: result.reason });
return result;
}
const availability = _effectiveFaderAvailability(participant);
if (availability !== 'available') {
const result = _degraded(`Fader is ${availability}`, _summaryFader(participant, { availability }));
_emitFaderUnavailable(participant, result.reason);
_recordOutcome({ domain: 'audio-mix', operation: 'get-fader-value', participantId: participant.participantId, faderId: participant.fader.id, outcome: result.outcome, status: availability, reason: result.reason });
return result;
}
const previous = participant.fader.currentValue;
try {
const handler = participant.operationHandlers['fader.get-value'];
const raw = handler ? await _withFaderTimeout(handler({ participant: _summaryParticipant(participant), fader: _summaryFader(participant) }), participant, 'fader.get-value') : previous;
const committed = _clamp(_extractCommittedValue(raw, previous), participant.fader.min, participant.fader.max);
participant.fader.currentValue = committed;
participant.fader.lastCommittedAt = _now();
_recordOutcome({ domain: 'audio-mix', operation: 'get-fader-value', participantId: participant.participantId, faderId: participant.fader.id, outcome: 'handled', status: 'available' });
_touch();
return _handled({ ..._summaryFader(participant), committedValue: committed, previousCommittedValue: previous });
} catch (err) {
const timedOut = !!(err && err.timedOut);
const reason = timedOut ? 'Fader get-value timed out' : _boundedReason(err && err.message ? err.message : String(err));
_recordOutcome({ domain: 'audio-mix', operation: 'get-fader-value', participantId: participant.participantId, faderId: participant.fader.id, outcome: 'failed', status: timedOut ? 'timeout' : 'failed', reason });
_touch();
return _failed(reason, { ..._summaryFader(participant), committedValue: previous, timedOut });
}
}
async function setFaderValue(payload = {}) {
const participant = _findParticipant(payload.participantId, payload.faderId || payload.id);
const requestedValue = _number(payload.value ?? payload.requestedValue, null);
if (!participant || !participant.fader) {
const result = _noHandler('Unknown audio-mix fader', { participantId: payload.participantId || '', faderId: payload.faderId || payload.id || '', requestedValue });
_recordOutcome({ domain: 'audio-mix', operation: 'set-fader-value', participantId: result.payload.participantId, faderId: result.payload.faderId, outcome: result.outcome, reason: result.reason });
return result;
}
if (!Number.isFinite(requestedValue)) {
const result = _denied('Fader value must be a finite number', { ..._summaryFader(participant), requestedValue });
_recordOutcome({ domain: 'audio-mix', operation: 'set-fader-value', participantId: participant.participantId, faderId: participant.fader.id, outcome: result.outcome, status: 'invalid-value', reason: result.reason });
return result;
}
const availability = _effectiveFaderAvailability(participant);
if (availability !== 'available') {
const result = _degraded(`Fader is ${availability}`, { ..._summaryFader(participant, { availability }), requestedValue });
_emitFaderUnavailable(participant, result.reason);
_recordOutcome({ domain: 'audio-mix', operation: 'set-fader-value', participantId: participant.participantId, faderId: participant.fader.id, outcome: result.outcome, status: availability, reason: result.reason });
return result;
}
const previous = participant.fader.currentValue;
const normalizedValue = _clamp(requestedValue, participant.fader.min, participant.fader.max);
participant.fader.lastRequestedValue = requestedValue;
try {
const handler = participant.operationHandlers['fader.set-value'];
const raw = handler ? await _withFaderTimeout(handler(normalizedValue, { requestedValue, participant: _summaryParticipant(participant), fader: _summaryFader(participant) }), participant, 'fader.set-value') : normalizedValue;
const committed = _clamp(_extractCommittedValue(raw, normalizedValue), participant.fader.min, participant.fader.max);
participant.fader.currentValue = committed;
participant.fader.lastRejectedValue = null;
participant.fader.lastCommittedAt = _now();
const result = { ..._summaryFader(participant), requestedValue, normalizedValue, committedValue: committed, previousCommittedValue: previous };
_recordOutcome({ domain: 'audio-mix', operation: 'set-fader-value', participantId: participant.participantId, faderId: participant.fader.id, outcome: 'handled', status: committed === normalizedValue ? 'committed' : 'normalized' });
capabilities.emitEvent('audio-mix', 'fader-value-changed', result);
_touch();
return _handled(result);
} catch (err) {
const timedOut = !!(err && err.timedOut);
const reason = timedOut ? 'Fader set-value timed out' : _boundedReason(err && err.message ? err.message : String(err));
participant.fader.lastRejectedValue = requestedValue;
participant.fader.currentValue = previous;
_recordOutcome({ domain: 'audio-mix', operation: 'set-fader-value', participantId: participant.participantId, faderId: participant.fader.id, outcome: 'failed', status: timedOut ? 'timeout' : 'failed', reason });
_touch();
return _failed(reason, { ..._summaryFader(participant), requestedValue, normalizedValue, committedValue: previous, previousCommittedValue: previous, timedOut });
}
}
function startSession(options = {}) {
// Close any open input sessions on the outgoing session before it is replaced, so a direct
// startSession() (e.g. a song switch that does not call stopSession first) doesn't orphan
// provider-owned capture with no state left to close it.
if (currentSession) _closeOpenInputSessions('Session restarted');
const previousSelectedInput = currentSession && currentSession.selectedInput ? _clone(currentSession.selectedInput) : null;
const previousSelectedMonitoringProvider = currentSession && currentSession.selectedMonitoringProvider ? _clone(currentSession.selectedMonitoringProvider) : null;
const previousDirectMonitor = currentSession && currentSession.directMonitor ? _clone(currentSession.directMonitor) : null;
const previousMonitoringSessions = currentSession ? Array.from(currentSession.monitoringSessions.values()).map(_clone) : [];
const previousStorageStatus = currentSession && currentSession.storageStatus;
currentSession = _newSession({ ...options, state: 'active' });
if (previousStorageStatus === 'failed') {
// Persistence is failed, so the stored key may be stale — keep the in-memory selection
// stable instead of silently reverting to whatever storage happens to hold.
if (previousSelectedInput) currentSession.selectedInput = previousSelectedInput;
if (previousSelectedMonitoringProvider) currentSession.selectedMonitoringProvider = previousSelectedMonitoringProvider;
if (previousDirectMonitor) currentSession.directMonitor = previousDirectMonitor;
currentSession.storageStatus = 'failed';
} else if (!currentSession.selectedInput && previousSelectedInput) {
currentSession.selectedInput = previousSelectedInput;
if (!currentSession.selectedMonitoringProvider && previousSelectedMonitoringProvider) currentSession.selectedMonitoringProvider = previousSelectedMonitoringProvider;
if (previousDirectMonitor) currentSession.directMonitor = previousDirectMonitor;
} else {
if (!currentSession.selectedMonitoringProvider && previousSelectedMonitoringProvider) currentSession.selectedMonitoringProvider = previousSelectedMonitoringProvider;
if (previousDirectMonitor) currentSession.directMonitor = previousDirectMonitor;
}
for (const participant of knownMixParticipants.values()) {
const attached = { ...participant, lastSeenAt: _now() };
if (attached.availability === 'pending') attached.availability = 'available';
if (attached.fader && attached.fader.availability === 'pending') attached.fader.availability = 'available';
currentSession.mixParticipants.set(attached.participantId, attached);
}
for (const provider of knownInputProviders.values()) {
currentSession.inputProviders.set(provider.providerId, { ...provider, lastSeenAt: _now() });
}
for (const source of knownInputSources.values()) {
currentSession.inputSources.set(source.sourceId, { ...source, lastSeenAt: _now(), selected: false });
}
for (const provider of knownMonitoringProviders.values()) {
currentSession.monitoringProviders.set(provider.providerId, { ...provider, lastSeenAt: _now() });
}
for (const session of previousMonitoringSessions) {
if (session && (session.state === 'active' || session.state === 'degraded')) currentSession.monitoringSessions.set(session.monitoringId, { ...session, updatedAt: _now() });
}
_refreshDuplicateSuppression();
_refreshInputDuplicateSuppression();
_refreshMonitoringDuplicateSuppression();
_recordOutcome({ domain: 'audio-mix', operation: 'session.start', participantId: OWNER_ID, outcome: 'handled' });
_recordOutcome({ domain: 'audio-input', operation: 'session.start', participantId: OWNER_ID, outcome: 'handled', status: currentSession.selectedInput ? currentSession.selectedInput.restoreStatus : 'not-selected' });
_recordOutcome({ domain: 'audio-monitoring', operation: 'session.start', participantId: OWNER_ID, outcome: 'handled', status: currentSession.monitoringSessions.size ? 'active' : 'stopped' });
_touch();
return snapshot();
}
function stopSession(reason = 'Session stopped') {
currentSession.state = 'stopped';
currentSession.routeState = _normalizeRoute({ routeKind: 'unknown', availability: 'unavailable', reason });
_closeOpenInputSessions(reason);
_recordOutcome({ domain: 'audio-mix', operation: 'session.stop', participantId: OWNER_ID, outcome: 'handled', reason });
_recordOutcome({ domain: 'audio-monitoring', operation: 'session.stop', participantId: OWNER_ID, outcome: 'handled', status: currentSession.monitoringSessions.size ? 'active' : 'stopped', reason: 'Playback stopped; live monitoring remains requester-owned' });
_touch();
return snapshot();
}
// Tear down any open input sessions so providers release live capture when the
// session stops (e.g. song switch) instead of leaving microphones/instruments hot.
// Provider close is best-effort and non-blocking — teardown must not depend on it.
function _closeOpenInputSessions(reason) {
if (!currentSession.openInputSessions.size) return;
const boundedReason = _boundedReason(reason);
// One pseudonymizer for the whole batch so distinct sources keep distinct source-NN ids
// across the emitted source-closed events instead of every event reusing source-01.
const pseudonymize = _newPseudonymizer();
for (const session of Array.from(currentSession.openInputSessions.values())) {
const provider = currentSession.inputProviders.get(session.providerId);
const handler = provider && provider.operationHandlers && provider.operationHandlers['source.close'];
if (typeof handler === 'function') {
try {
Promise.resolve(handler({ sourceId: session.sourceId, logicalSourceKey: session.logicalSourceKey, openSessionId: session.openSessionId, requesterIds: session.requesters.map(item => item.requesterId) })).catch(() => {});
} catch (err) { /* best-effort teardown */ }
}
session.state = 'closed';
session.closedAt = _now();
session.reason = boundedReason;
session.requesters = [];
_recordOutcome({ domain: 'audio-input', operation: 'close-source', participantId: OWNER_ID, providerId: session.providerId, sourceId: session.sourceId, logicalSourceKey: session.logicalSourceKey, openSessionId: session.openSessionId, outcome: 'handled', status: 'closed', reason: boundedReason });
capabilities.emitEvent('audio-input', 'source-closed', _redactedOpenSession(session, pseudonymize));
}
currentSession.openInputSessions.clear();
}
function setRoute(route) {
currentSession.routeState = _normalizeRoute(route);
const degraded = currentSession.routeState.availability === 'degraded' || currentSession.routeState.availability === 'unavailable';
_recordOutcome({
domain: 'audio-mix',
operation: 'route.set',
participantId: OWNER_ID,
outcome: degraded ? 'degraded' : 'handled',
status: currentSession.routeState.availability,
reason: currentSession.routeState.fallbackReason,
});
// Redact the device identity before it reaches event subscribers or the
// caller; raw deviceLabel/deviceId only ever live in currentSession.
// Snapshot the return clone before dispatching the (mutable) event payload
// so a subscriber can't desync or mutate the caller's copy.
const redactedRoute = _redactedRoute(currentSession.routeState, _newPseudonymizer());
const returnedRoute = _clone(redactedRoute);
capabilities.emitEvent('audio-mix', degraded ? 'route-degraded' : 'route-changed', redactedRoute);
_touch();
return returnedRoute;
}
function setAnalyser(analyser) {
currentSession.analyserState = _normalizeAnalyser(analyser);
const degraded = currentSession.analyserState.availability !== 'available';
_recordOutcome({ domain: 'audio-mix', operation: 'analyser.set', participantId: currentSession.analyserState.participantId || OWNER_ID, outcome: degraded ? 'degraded' : 'handled', status: currentSession.analyserState.availability, reason: currentSession.analyserState.reason });
capabilities.emitEvent('audio-mix', degraded ? 'analyser-unavailable' : 'analyser-changed', currentSession.analyserState);
_touch();
return _clone(currentSession.analyserState);
}
function registerMixParticipant(spec) {
const source = _plainObject(spec);
if (source.incompatible || (source.version && Number(source.version) !== 1)) {
const result = _incompatibleVersion('Mix participant requires audio-session contract version 1');
_recordOutcome({ domain: 'audio-mix', operation: 'register-participant', participantId: source.participantId || source.id, outcome: result.outcome, reason: result.reason });
return result;
}
const participant = _normalizeParticipant(source);
if (!participant) {
const result = _failed('Mix participant registration requires participantId');
_recordOutcome({ domain: 'audio-mix', operation: 'register-participant', outcome: result.outcome, reason: result.reason });
return result;
}
// Preserve operation handlers across re-registration, the same way
// registerInputSource and the monitoring provider already do. Without
// this, a caller that re-registers to refresh availability / current
// value but doesn't re-pass getValue/setValue would silently drop the
// fader handlers — which is exactly how the song fader's volume-apply
// handler was being wiped on every song load. New handlers still
// override; only ones the new spec omits are retained.
const _existingParticipant = knownMixParticipants.get(participant.participantId);
if (_existingParticipant && _existingParticipant.operationHandlers) {
participant.operationHandlers = {
..._existingParticipant.operationHandlers,
...participant.operationHandlers,
};
}
participant.logicalFaderKey = _logicalFaderKey(participant, source);
if (participant.fader && participant.fader.availability === 'available' && participant.availability !== 'available') {
participant.fader.availability = participant.availability;
}
knownMixParticipants.set(participant.participantId, participant);
currentSession.mixParticipants.set(participant.participantId, participant);
_refreshDuplicateSuppression();
_recordDuplicateSuppression();
_recordOutcome({ domain: 'audio-mix', operation: 'register-participant', participantId: participant.participantId, outcome: 'handled', status: participant.availability });
capabilities.emitEvent('audio-mix', 'participant-registered', _summaryParticipant(participant));
_touch();
return _handled(_summaryParticipant(participant));
}
function unregisterMixParticipant(participantId) {
const id = _string(participantId);
const removed = currentSession.mixParticipants.delete(id);
knownMixParticipants.delete(id);
const outcome = removed ? 'handled' : 'no-handler';
_recordOutcome({ domain: 'audio-mix', operation: 'unregister-participant', participantId: id, outcome });
if (removed) capabilities.emitEvent('audio-mix', 'participant-removed', { participantId: id });
_touch();
return removed ? _handled({ participantId: id, removed: true }) : _noHandler(`Unknown mix participant: ${id}`, { participantId: id, removed: false });
}
function registerInputSource(spec) {
const source = _plainObject(spec);
if (source.incompatible || (source.version && Number(source.version) !== 1)) {
const result = _incompatibleVersion('Input source requires audio-session contract version 1');
_recordOutcome({ domain: 'audio-input', operation: 'register-source', participantId: source.providerId, providerId: source.providerId, outcome: result.outcome, reason: result.reason });
_touch();
return result;
}
const input = _normalizeSource(source);
if (!input) {
const result = _failed('Input source registration requires sourceId, providerId, and logicalSourceKey');
_recordOutcome({ domain: 'audio-input', operation: 'register-source', outcome: result.outcome, reason: result.reason });
_touch();
return result;
}
// The logical key is surfaced verbatim in redaction-safe diagnostics, so the contract
// requires it to already be redaction-safe. Reject a key that _boundedReason would alter
// (a path, token, whitespace, or over-long value) rather than silently leaking it.
const boundedLogicalKey = _boundedReason(input.logicalSourceKey);
if (boundedLogicalKey !== input.logicalSourceKey) {
const result = _failed('Input source logicalSourceKey must be redaction-safe');
_recordOutcome({ domain: 'audio-input', operation: 'register-source', participantId: input.providerId, providerId: input.providerId, logicalSourceKey: boundedLogicalKey, outcome: result.outcome, status: 'unsafe-key', reason: result.reason });
_touch();
return result;
}
// sourceId is the global Map key, so a sourceId already owned by a different provider would
// silently overwrite that provider's record. Reject the collision instead — providers must
// namespace sourceId to be globally unique (the stable cross-provider handle is logicalSourceKey).
const existingById = currentSession.inputSources.get(input.sourceId);
if (existingById && existingById.providerId !== input.providerId) {
const result = _failed('Input source sourceId collides with another provider; sourceId must be globally unique');
_recordOutcome({ domain: 'audio-input', operation: 'register-source', participantId: input.providerId, providerId: input.providerId, sourceId: input.sourceId, logicalSourceKey: input.logicalSourceKey, outcome: result.outcome, status: 'sourceid-collision', reason: result.reason });
_touch();
return result;
}
const provider = _normalizeInputProvider(source, input);
const existingByRecord = Array.from(currentSession.inputSources.values()).find(item => _sourceRecordKey(item) === _sourceRecordKey(input));
const existing = currentSession.inputSources.get(input.sourceId) || existingByRecord || null;
const previousAvailability = existing && existing.availability;
if (existing && existing.sourceId !== input.sourceId) {
currentSession.inputSources.delete(existing.sourceId);
knownInputSources.delete(existing.sourceId);
}
if (existing) {
input.registeredAt = existing.registeredAt;
input.selected = existing.selected;
}
currentSession.inputSources.set(input.sourceId, input);
knownInputSources.set(input.sourceId, input);
_refreshInputDuplicateSuppression();
_recordInputDuplicateSuppression();
const pseudonymize = _newPseudonymizer();
const summary = _redactedSource(input, pseudonymize);
const returned = _clone(summary);
_recordOutcome({ domain: 'audio-input', operation: 'register-source', participantId: input.providerId, providerId: provider.providerId, sourceId: input.sourceId, logicalSourceKey: input.logicalSourceKey, outcome: 'handled', status: input.availability });
capabilities.emitEvent('audio-input', 'source-registered', summary);
if (previousAvailability && previousAvailability !== input.availability) capabilities.emitEvent('audio-input', 'source-availability-changed', summary);
_touch();
return _handled(returned);
}
function unregisterInputSource(query) {
const payload = typeof query === 'string' ? { sourceId: query } : _plainObject(query);
const source = _findInputSource(payload);
const removed = source ? currentSession.inputSources.delete(source.sourceId) : false;
if (source) knownInputSources.delete(source.sourceId);
const outcome = removed ? 'handled' : 'no-handler';
_refreshInputDuplicateSuppression();
_recordOutcome({ domain: 'audio-input', operation: 'unregister-source', participantId: source && source.providerId, providerId: source && source.providerId, sourceId: source && source.sourceId, logicalSourceKey: source && source.logicalSourceKey, outcome });
const pseudonymize = _newPseudonymizer();
// Only a raw sourceId is pseudonymized; logicalSourceKey/providerId are redaction-safe and
// echoed back so a miss still tells the caller which source it was looking for.
const resolvedSourceId = source ? source.sourceId : _string(payload.sourceId || payload.id);
const redacted = {
sourceId: resolvedSourceId ? pseudonymize(resolvedSourceId, 'source') : '',
// On a miss these come straight from an untrusted caller — bound them so a path/token
// cannot land in the echoed payload or diagnostics. Resolved-source values are safe.
logicalSourceKey: source ? source.logicalSourceKey : _boundedReason(payload.logicalSourceKey || payload.logicalKey || payload.sourceKey),
providerId: source ? source.providerId : _boundedReason(payload.providerId),
removed,
};
if (removed) capabilities.emitEvent('audio-input', 'source-removed', redacted);
_touch();
return removed ? _handled(_clone(redacted)) : _noHandler('Unknown input source', redacted);
}
function selectInputSource(query, requester = 'unknown') {
const payload = typeof query === 'string' ? { sourceId: query } : _plainObject(query);
const selected = _findInputSource(payload);
// Bound/sanitize the requester before it reaches redaction-safe diagnostics, consistent
// with open/close-source attribution.
const requesterId = _requesterId(requester, 'unknown');
// A caller-provided logical key is only *supposed* to be redaction-safe; bound it before it
// reaches diagnostics or the echoed payload in case the caller passed a path/token.
const requestedLogicalKey = _boundedReason(payload.logicalSourceKey || payload.logicalKey || payload.sourceKey);
const requestedSourceId = _string(payload.sourceId || payload.id);
if (!selected) {
const missingSourceId = requestedSourceId ? _newPseudonymizer()(requestedSourceId, 'source') : '';
const result = _degraded('Input source is unavailable', { logicalSourceKey: requestedLogicalKey, sourceId: missingSourceId, availability: 'unavailable' });
_recordOutcome({ domain: 'audio-input', operation: 'select-source', participantId: requesterId, requesterId, logicalSourceKey: requestedLogicalKey, outcome: result.outcome, status: 'unavailable', reason: result.reason });
_touch();
return result;
}
currentSession.selectedInput = _makeSelectedInput(selected, requesterId);
currentSession.storageStatus = _writeSelectedLogicalSourceKey(selected.logicalSourceKey);
_syncSelectedInput();
const summary = _redactedSelectedInput(currentSession.selectedInput, _newPseudonymizer());
_recordOutcome({ domain: 'audio-input', operation: 'select-source', participantId: requesterId, requesterId, providerId: selected.providerId, sourceId: selected.sourceId, logicalSourceKey: selected.logicalSourceKey, outcome: 'handled', status: selected.availability });
capabilities.emitEvent('audio-input', 'source-selected', summary);
_touch();
return _handled(summary);
}
function listInputSources(payload = {}) {
const includeUnavailable = payload.includeUnavailable !== false;
const pseudonymize = _newPseudonymizer();
const sources = _visibleInputSources()
.filter(source => includeUnavailable || source.availability === 'available' || (currentSession.selectedInput && source.logicalSourceKey === currentSession.selectedInput.logicalSourceKey))
.map(source => _redactedSource(source, pseudonymize));
_recordOutcome({ domain: 'audio-input', operation: 'list-sources', participantId: OWNER_ID, outcome: 'handled', status: `${sources.length}` });
return _handled({ sources, selected: _redactedSelectedInput(_syncSelectedInput(), pseudonymize), timeoutMs: INPUT_OPERATION_TIMEOUT_MS });
}
function inspectInput(payload = {}) {
const snapshotData = snapshot().domains['audio-input'];
if (payload.sourceId || payload.logicalSourceKey) {
// Snapshot sourceIds are per-snapshot pseudonyms, so a raw sourceId can't match them.
// Resolve the request to its source and pin the snapshot entry by stable, redaction-safe
// identity (logicalSourceKey + provider + mode) — the snapshot still contains suppressed
// duplicates, so a logicalSourceKey-only match could return a compatibility loser.
const resolved = _findInputSource(payload);
const logicalSourceKey = resolved ? resolved.logicalSourceKey : _string(payload.logicalSourceKey || payload.logicalKey || payload.sourceKey);
const match = resolved
? snapshotData.sources.find(source => source.logicalSourceKey === resolved.logicalSourceKey && source.providerId === resolved.providerId && source.sourceMode === resolved.sourceMode)
: (logicalSourceKey ? snapshotData.sources.find(source => source.logicalSourceKey === logicalSourceKey) : null);
return _handled({ ...snapshotData, source: match || null });
}
return _handled(snapshotData);
}
async function enumerateInputSources(options = {}) {
const payload = _plainObject(options);
if (payload.explicit !== true && payload.userInitiated !== true) {
const result = _denied('Source enumeration requires an explicit user/provider request');
_recordOutcome({ domain: 'audio-input', operation: 'source.enumerate', participantId: payload.providerId || OWNER_ID, providerId: payload.providerId, outcome: result.outcome, reason: result.reason });
_touch();
return result;
}
const providers = Array.from(currentSession.inputProviders.values()).filter(provider => !payload.providerId || provider.providerId === payload.providerId);
if (!providers.length) {
const result = _noOwner('No input provider is available for source enumeration');
_recordOutcome({ domain: 'audio-input', operation: 'source.enumerate', participantId: payload.providerId || OWNER_ID, providerId: payload.providerId, outcome: result.outcome, reason: result.reason });
_touch();
return result;
}
const registered = [];
// Re-redact every registered source with one pseudonymizer so distinct sources get distinct,
// stable pseudonyms across the whole enumerate response. registerInputSource() redacts each
// payload with its own fresh pseudonymizer, which would otherwise emit duplicate `source-01`s.
const pseudonymize = _newPseudonymizer();
let handledAny = false;
for (const provider of providers) {
const handler = provider.operationHandlers && provider.operationHandlers['source.enumerate'];
if (typeof handler !== 'function') continue;
handledAny = true;
try {
const raw = await _withInputTimeout(handler({ providerId: provider.providerId, explicit: true, requesterId: _requesterId(payload.requesterId || payload.requester, 'enumerate') }), 'source.enumerate');
const result = _providerOutcome(raw);
// Preserve a provider's explicit non-handled outcome exactly (denied/failed/degraded/
// no-owner/no-handler/unsupported-command/incompatible-version/overridden) instead of
// treating it as an empty success or collapsing it to a generic degrade.
if (result.outcome !== 'handled') {
const reason = _boundedReason(result.reason) || `Source enumeration ${result.outcome}`;
_recordOutcome({ domain: 'audio-input', operation: 'source.enumerate', participantId: provider.providerId, providerId: provider.providerId, outcome: result.outcome, status: result.status || result.outcome, reason });
_touch();
return { outcome: result.outcome, reason, payload: { providerId: provider.providerId } };
}
const list = Array.isArray(result.payload && result.payload.sources) ? result.payload.sources : [];
for (const item of list) {
const response = registerInputSource({ ...item, providerId: item.providerId || provider.providerId, ownerPluginId: item.ownerPluginId || provider.ownerPluginId });
if (response.outcome !== 'handled') continue;
const stored = currentSession.inputSources.get(_string(item.sourceId || item.id));
registered.push(stored ? _redactedSource(stored, pseudonymize) : response.payload);
}
} catch (err) {
const timedOut = !!(err && err.timedOut);
const reason = timedOut ? 'Source enumeration timed out' : _boundedReason(err && err.message ? err.message : String(err));
_recordOutcome({ domain: 'audio-input', operation: 'source.enumerate', participantId: provider.providerId, providerId: provider.providerId, outcome: 'failed', status: timedOut ? 'timeout' : 'failed', reason });
_touch();
return _failed(reason, { providerId: provider.providerId, timedOut });
}
}
if (!handledAny) {
// Providers exist but none expose a source.enumerate handler, so the explicit request
// was a no-op — report it as no-handler rather than an empty success.
const result = _noHandler('No input provider supports source enumeration');
_recordOutcome({ domain: 'audio-input', operation: 'source.enumerate', participantId: payload.providerId || OWNER_ID, providerId: payload.providerId, outcome: result.outcome, status: 'no-handler', reason: result.reason });
_touch();
return result;
}
_recordOutcome({ domain: 'audio-input', operation: 'source.enumerate', participantId: payload.providerId || OWNER_ID, providerId: payload.providerId, outcome: 'handled', status: `${registered.length}` });
_touch();
return _handled({ sources: registered });
}
async function openInputSource(payload = {}, requester = 'unknown') {
const source = _plainObject(payload);
// Identity comes from the authenticated dispatch caller, never the payload —
// a payload-supplied requesterId could otherwise spoof another requester and
// misattribute or release shared open sessions it does not own.
const requesterId = _requesterId(requester, 'unknown');
const selectedInput = currentSession.selectedInput;
if (!selectedInput) {
const session = _inputSessionSummary({ requesterId, purpose: source.purpose, state: 'unavailable', reason: 'No selected input source is available' }, _newPseudonymizer());
const result = _noOwner('No selected input source is available', session);
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, outcome: result.outcome, status: 'not-selected', reason: result.reason });
capabilities.emitEvent('audio-input', 'source-open-degraded', session);
_touch();
return result;
}
// open-source uses the selected source by default; an explicit logicalSourceKey/sourceId
// may disambiguate but must resolve to the selected source — it must not silently switch.
// Resolve the canonical winner for the selected logical key (duplicate suppression may have
// changed which concrete source wins since selection), and compare the hint against its
// sourceId — not just logicalSourceKey — so a non-selected native/compatibility duplicate
// sharing the same logical key cannot be opened by passing its sourceId.
const selectedSource = _findInputSource({ logicalSourceKey: selectedInput.logicalSourceKey });
const hasHint = !!(source.logicalSourceKey || source.sourceId);
const selected = hasHint ? _findInputSource(source) : selectedSource;
if (!selectedSource) {
// A source is selected but no matching source is registered (e.g. a restored selection
// before its provider hydrates, or the provider unregistered). Report it as unavailable
// rather than implying the requester asked for a different source.
const selectedKey = _boundedReason(selectedInput.logicalSourceKey);
const session = _inputSessionSummary({ logicalSourceKey: selectedKey, requesterId, purpose: source.purpose, state: 'unavailable', reason: 'Selected input source is unavailable' }, _newPseudonymizer());
const result = _degraded('Selected input source is unavailable', session);
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, logicalSourceKey: selectedKey, outcome: result.outcome, status: 'unavailable', reason: result.reason });
capabilities.emitEvent('audio-input', 'source-open-degraded', session);
_touch();
return result;
}
if (!selected || selected.sourceId !== selectedSource.sourceId) {
// Bound the caller-provided hint key, and echo a pseudonymized sourceId hint (callers often
// disambiguate by sourceId) so the payload/snapshot can correlate it without leaking it.
const hintKey = _boundedReason(source.logicalSourceKey);
const hintSourceId = _string(source.sourceId || source.id);
const session = _inputSessionSummary({ source: { logicalSourceKey: hintKey, sourceId: hintSourceId }, requesterId, purpose: source.purpose, state: 'unavailable', reason: 'Requested input source is not the selected source' }, _newPseudonymizer());
const result = _degraded('Requested input source is not the selected source', session);
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, sourceId: hintSourceId, logicalSourceKey: hintKey, outcome: result.outcome, status: 'unavailable', reason: result.reason });
capabilities.emitEvent('audio-input', 'source-open-degraded', session);
_touch();
return result;
}
if (selected.availability !== 'available') {
// Emit an OpenInputSessionSummary-shaped payload (with requester attribution) for a
// denied/unavailable selected source too, so consumers see one consistent event schema
// across pre-marked and provider-reported denials.
const reason = _boundedReason(selected.reason) || (selected.availability === 'denied' ? 'Input permission denied' : `Input source is ${selected.availability}`);
const session = _inputSessionSummary({ source: selected, requesterId, purpose: source.purpose, channelShape: selected.channelSummary && selected.channelSummary.channelShape, state: selected.availability, reason }, _newPseudonymizer());
const result = selected.availability === 'denied' ? _denied(reason, session) : _degraded(reason, session);
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, providerId: selected.providerId, sourceId: selected.sourceId, logicalSourceKey: selected.logicalSourceKey, outcome: result.outcome, status: selected.availability, reason: result.reason });
capabilities.emitEvent('audio-input', result.outcome === 'denied' ? 'permission-denied' : 'source-open-degraded', session);
_touch();
return result;
}
const requiredShape = _requiredChannelShape(source.requiredChannelShape || source.channelShape || source.channel);
const compatible = _channelCompatible(selected.channelSummary, requiredShape);
if (!compatible.ok) {
const session = _inputSessionSummary({ source: selected, requesterId, purpose: source.purpose, channelShape: compatible.channelShape, state: 'incompatible', reason: 'Selected source cannot satisfy requested channel shape' }, _newPseudonymizer());
const result = _degraded(session.reason, session);
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, providerId: selected.providerId, sourceId: selected.sourceId, logicalSourceKey: selected.logicalSourceKey, outcome: result.outcome, status: 'incompatible', reason: result.reason });
capabilities.emitEvent('audio-input', 'source-open-degraded', session);
_touch();
return result;
}
const channelShape = compatible.channelShape;
const key = _openSessionKey(selected.logicalSourceKey, channelShape);
const existing = currentSession.openInputSessions.get(key);
if (existing && existing.state === 'open') {
if (!existing.requesters.some(item => item.requesterId === requesterId)) existing.requesters.push(_requesterRef(requesterId, source.purpose));
existing.lastUsedAt = _now();
const summary = _redactedOpenSession(existing, _newPseudonymizer());
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, providerId: selected.providerId, sourceId: selected.sourceId, logicalSourceKey: selected.logicalSourceKey, openSessionId: existing.openSessionId, outcome: 'handled', status: 'open' });
_touch();
return _handled(summary);
}
const provider = currentSession.inputProviders.get(selected.providerId);
if (!provider) {
const session = _inputSessionSummary({ source: selected, requesterId, purpose: source.purpose, state: 'no-owner', reason: 'No provider owns the selected input source' }, _newPseudonymizer());
const result = _noOwner('No provider owns the selected input source', session);
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, sourceId: selected.sourceId, logicalSourceKey: selected.logicalSourceKey, outcome: result.outcome, status: 'no-owner', reason: result.reason });
capabilities.emitEvent('audio-input', 'source-open-degraded', session);
_touch();
return result;
}
if (provider.version !== 1) {
const result = _incompatibleVersion('Input provider requires audio-session contract version 1');
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, providerId: provider.providerId, sourceId: selected.sourceId, logicalSourceKey: selected.logicalSourceKey, outcome: result.outcome, status: 'incompatible-version', reason: result.reason });
_touch();
return result;
}
const handler = provider.operationHandlers && provider.operationHandlers['source.open'];
if (!provider.operations.includes('source.open')) {
const result = _unsupportedCommand('Selected input provider does not support source.open');
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, providerId: provider.providerId, sourceId: selected.sourceId, logicalSourceKey: selected.logicalSourceKey, outcome: result.outcome, status: 'unsupported-command', reason: result.reason });
_touch();
return result;
}
if (typeof handler !== 'function') {
const result = _noHandler('Selected input provider has no source.open handler');
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, providerId: provider.providerId, sourceId: selected.sourceId, logicalSourceKey: selected.logicalSourceKey, outcome: result.outcome, status: 'no-handler', reason: result.reason });
_touch();
return result;
}
const openSession = {
openSessionId: _id('input-open'),
logicalSourceKey: selected.logicalSourceKey,
sourceId: selected.sourceId,
providerId: selected.providerId,
channelShape,
state: 'opening',
requesters: [_requesterRef(requesterId, source.purpose)],
openedAt: '',
lastUsedAt: _now(),
closedAt: '',
reason: '',
key,
};
try {
const raw = await _withInputTimeout(handler({ sourceId: selected.sourceId, logicalSourceKey: selected.logicalSourceKey, requesterId, requiredChannelShape: channelShape, purpose: source.purpose }), 'source.open');
const providerResult = _providerOutcome(raw);
openSession.state = providerResult.outcome === 'handled' ? 'open' : (providerResult.status || providerResult.outcome);
openSession.reason = providerResult.reason;
if (providerResult.outcome === 'handled') {
openSession.openedAt = _now();
currentSession.openInputSessions.set(key, openSession);
const summary = _redactedOpenSession(openSession, _newPseudonymizer());
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, providerId: provider.providerId, sourceId: selected.sourceId, logicalSourceKey: selected.logicalSourceKey, openSessionId: openSession.openSessionId, outcome: 'handled', status: 'open' });
capabilities.emitEvent('audio-input', 'source-opened', summary);
_touch();
return _handled(summary);
}
const summary = _redactedOpenSession(openSession, _newPseudonymizer());
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, providerId: provider.providerId, sourceId: selected.sourceId, logicalSourceKey: selected.logicalSourceKey, openSessionId: openSession.openSessionId, outcome: providerResult.outcome, status: openSession.state, reason: providerResult.reason });
capabilities.emitEvent('audio-input', providerResult.outcome === 'denied' ? 'permission-denied' : 'source-open-degraded', summary);
_touch();
if (providerResult.outcome === 'denied') return _denied(providerResult.reason || 'Input permission denied', summary);
if (providerResult.outcome === 'failed') return _failed(providerResult.reason || 'Input provider failed', summary);
// Preserve the provider's exact non-handled outcome (no-handler/no-owner/unsupported-command/
// incompatible-version/overridden/degraded) instead of collapsing it to a generic degrade.
return { outcome: providerResult.outcome, reason: providerResult.reason || `Input open returned ${providerResult.outcome}`, payload: summary };
} catch (err) {
const timedOut = !!(err && err.timedOut);
openSession.state = 'failed';
openSession.reason = timedOut ? 'Input source open timed out' : _boundedReason(err && err.message ? err.message : String(err));
const summary = _redactedOpenSession(openSession, _newPseudonymizer());
_recordOutcome({ domain: 'audio-input', operation: 'open-source', participantId: requesterId, requesterId, providerId: provider.providerId, sourceId: selected.sourceId, logicalSourceKey: selected.logicalSourceKey, openSessionId: openSession.openSessionId, outcome: 'failed', status: timedOut ? 'timeout' : 'failed', reason: openSession.reason });
capabilities.emitEvent('audio-input', 'source-open-degraded', summary);
_touch();
return _failed(openSession.reason, { ...summary, timedOut });
}
}
async function closeInputSource(payload = {}, requester = 'unknown') {
const source = _plainObject(payload);
// Identity comes from the authenticated dispatch caller, never the payload, so a
// caller cannot release a shared open-session reference it does not own.
const requesterId = _requesterId(requester, 'unknown');
// openSessionId may be caller-supplied on a miss; bound it (a generated `input-open-<n>` id is
// unchanged) so it can't leak a path/token into diagnostics, and still match for lookup.
const openSessionId = _boundedReason(source.openSessionId || source.id);
// Accept the same logicalSourceKey aliases the other audio-input paths do.
const logicalSourceKey = _string(source.logicalSourceKey || source.logicalKey || source.sourceKey);
const hasExplicitChannelShape = source.requiredChannelShape != null || source.channelShape != null || source.channel != null;
const requiredShape = _requiredChannelShape(source.requiredChannelShape || source.channelShape || source.channel);
const key = logicalSourceKey ? _openSessionKey(logicalSourceKey, requiredShape) : '';
let session = null;
if (openSessionId) {
session = Array.from(currentSession.openInputSessions.values()).find(item => item.openSessionId === openSessionId) || null;
} else if (key && currentSession.openInputSessions.has(key)) {
session = currentSession.openInputSessions.get(key);
} else if (logicalSourceKey && !hasExplicitChannelShape) {
// requiredChannelShape is optional, so the exact channel-shape key may not match the
// shape the session was opened with. Only when the caller omitted the shape, fall back
// to the logical source key — unambiguous only when a single session is open for it;
// otherwise require shape/openSessionId. When a shape WAS given but didn't match, fall
// through to "no match" rather than closing a differently-shaped session.
const matches = Array.from(currentSession.openInputSessions.values()).filter(item => item.logicalSourceKey === logicalSourceKey);
if (matches.length === 1) {
session = matches[0];
} else if (matches.length > 1) {
const result = _failed('Multiple open input sessions match the logical source key; specify requiredChannelShape or openSessionId');
_recordOutcome({ domain: 'audio-input', operation: 'close-source', participantId: requesterId, requesterId, logicalSourceKey, outcome: result.outcome, status: 'ambiguous', reason: result.reason });
_touch();
return result;
}
}
if (!session) {
const result = _noHandler('No matching open input session exists');
_recordOutcome({ domain: 'audio-input', operation: 'close-source', participantId: requesterId, requesterId, openSessionId, outcome: result.outcome, status: 'missing', reason: result.reason });
_touch();
return result;
}
session.requesters = session.requesters.filter(item => item.requesterId !== requesterId);
session.lastUsedAt = _now();
if (session.requesters.length) {
const summary = _redactedOpenSession(session, _newPseudonymizer());
_recordOutcome({ domain: 'audio-input', operation: 'close-source', participantId: requesterId, requesterId, providerId: session.providerId, sourceId: session.sourceId, logicalSourceKey: session.logicalSourceKey, openSessionId: session.openSessionId, outcome: 'handled', status: 'open' });
_touch();
return _handled(summary);
}
const provider = currentSession.inputProviders.get(session.providerId);
const handler = provider && provider.operationHandlers && provider.operationHandlers['source.close'];
if (!provider || typeof handler !== 'function') {
session.state = 'failed';
session.reason = 'Selected input provider has no source.close handler';
const summary = _redactedOpenSession(session, _newPseudonymizer());
const result = _noHandler(session.reason, summary);
_recordOutcome({ domain: 'audio-input', operation: 'close-source', participantId: requesterId, requesterId, providerId: session.providerId, sourceId: session.sourceId, logicalSourceKey: session.logicalSourceKey, openSessionId: session.openSessionId, outcome: result.outcome, status: 'no-handler', reason: result.reason });
_touch();
return result;
}
session.state = 'closing';
try {
const raw = await _withInputTimeout(handler({ sourceId: session.sourceId, logicalSourceKey: session.logicalSourceKey, openSessionId: session.openSessionId, requesterIds: [requesterId] }), 'source.close');
const providerResult = _providerOutcome(raw);
if (providerResult.outcome !== 'handled') {
session.state = providerResult.status || providerResult.outcome;
session.reason = providerResult.reason;
const summary = _redactedOpenSession(session, _newPseudonymizer());
_recordOutcome({ domain: 'audio-input', operation: 'close-source', participantId: requesterId, requesterId, providerId: session.providerId, sourceId: session.sourceId, logicalSourceKey: session.logicalSourceKey, openSessionId: session.openSessionId, outcome: providerResult.outcome, status: session.state, reason: providerResult.reason });
_touch();
if (providerResult.outcome === 'failed') return _failed(providerResult.reason || 'Input close failed', summary);
// Preserve the provider's exact non-handled outcome instead of collapsing it to degraded.
return { outcome: providerResult.outcome, reason: providerResult.reason || `Input close returned ${providerResult.outcome}`, payload: summary };
}
session.state = 'closed';
session.closedAt = _now();
const summary = _redactedOpenSession(session, _newPseudonymizer());
currentSession.openInputSessions.delete(session.key || _openSessionKey(session.logicalSourceKey, session.channelShape));
_recordOutcome({ domain: 'audio-input', operation: 'close-source', participantId: requesterId, requesterId, providerId: session.providerId, sourceId: session.sourceId, logicalSourceKey: session.logicalSourceKey, openSessionId: session.openSessionId, outcome: 'handled', status: 'closed' });
capabilities.emitEvent('audio-input', 'source-closed', summary);
_touch();
return _handled(summary);
} catch (err) {
const timedOut = !!(err && err.timedOut);
session.state = 'failed';
session.reason = timedOut ? 'Input source close timed out' : _boundedReason(err && err.message ? err.message : String(err));
const summary = _redactedOpenSession(session, _newPseudonymizer());
_recordOutcome({ domain: 'audio-input', operation: 'close-source', participantId: requesterId, requesterId, providerId: session.providerId, sourceId: session.sourceId, logicalSourceKey: session.logicalSourceKey, openSessionId: session.openSessionId, outcome: 'failed', status: timedOut ? 'timeout' : 'failed', reason: session.reason });
_touch();
return _failed(session.reason, { ...summary, timedOut });
}
}
function registerMonitoringProvider(spec) {
const source = _plainObject(spec);
if (source.incompatible || (source.version && Number(source.version) !== 1)) {
const result = _incompatibleVersion('Monitoring provider requires audio-session contract version 1');
_recordOutcome({ domain: 'audio-monitoring', operation: 'register-provider', participantId: source.providerId || source.id, providerId: source.providerId || source.id, outcome: result.outcome, reason: result.reason });
return result;
}
const provider = _normalizeMonitoringProvider(source);
if (!provider) {
const result = _failed('Monitoring provider registration requires providerId');
_recordOutcome({ domain: 'audio-monitoring', operation: 'register-provider', outcome: result.outcome, reason: result.reason });
return result;
}
// logicalMonitoringKey is surfaced verbatim via list/inspect and persisted, so reject a key
// that isn't redaction-safe (path/token) the same way audio-input guards logicalSourceKey.
if (provider.logicalMonitoringKey && _boundedReason(provider.logicalMonitoringKey) !== provider.logicalMonitoringKey) {
const result = _failed('Monitoring provider logicalMonitoringKey must be redaction-safe');
_recordOutcome({ domain: 'audio-monitoring', operation: 'register-provider', participantId: provider.providerId, providerId: provider.providerId, outcome: result.outcome, status: 'unsafe-key', reason: result.reason });
return result;
}
// providerId is the Map key surfaced in diagnostics/events; a different plugin claiming an
// already-owned providerId must fail rather than silently overwrite it (mirrors audio-input's
// sourceId-collision guard). Re-registration by the same owner is the normal idempotent update.
const ownerCollision = currentSession.monitoringProviders.get(provider.providerId) || knownMonitoringProviders.get(provider.providerId);
// Compute the effective new owner from the registration itself, defaulting to providerId — NOT
// inheriting the existing owner the way _normalizeMonitoringProvider does. Otherwise a colliding
// registration that omits ownerPluginId would silently inherit the original owner and bypass this.
const effectiveNewOwner = _string(source.ownerPluginId || source.pluginId, '') || provider.providerId;
if (ownerCollision && ownerCollision.ownerPluginId && ownerCollision.ownerPluginId !== effectiveNewOwner) {
const result = _failed(`Monitoring provider ${provider.providerId} is already registered by ${_boundedReason(ownerCollision.ownerPluginId)}`);
_recordOutcome({ domain: 'audio-monitoring', operation: 'register-provider', participantId: provider.providerId, providerId: provider.providerId, outcome: result.outcome, status: 'providerid-collision', reason: result.reason });
return result;
}
const existingByKey = Array.from(currentSession.monitoringProviders.values()).find(item => item.logicalMonitoringKey === provider.logicalMonitoringKey && item.sourceMode === provider.sourceMode && item.providerId === provider.providerId);
const existing = currentSession.monitoringProviders.get(provider.providerId) || existingByKey || null;
const previousAvailability = existing && existing.availability;
if (existing) provider.registeredAt = existing.registeredAt;
currentSession.monitoringProviders.set(provider.providerId, provider);
knownMonitoringProviders.set(provider.providerId, provider);
_refreshMonitoringDuplicateSuppression();
_recordMonitoringDuplicateSuppression();
const summary = _summaryMonitoringProvider(provider);
_recordOutcome({ domain: 'audio-monitoring', operation: 'register-provider', participantId: provider.providerId, providerId: provider.providerId, outcome: 'handled', status: provider.availability });
capabilities.emitEvent('audio-monitoring', 'provider-registered', summary);
if (previousAvailability && previousAvailability !== provider.availability) capabilities.emitEvent('audio-monitoring', 'provider-availability-changed', summary);
_touch();
return _handled(summary);
}
function unregisterMonitoringProvider(query) {
const payload = typeof query === 'string' ? { providerId: query } : _plainObject(query);
const provider = _findMonitoringProvider(payload);
const id = provider ? provider.providerId : _string(payload.providerId || payload.id || payload.logicalMonitoringKey);
const removed = provider ? currentSession.monitoringProviders.delete(provider.providerId) : false;
if (provider) knownMonitoringProviders.delete(provider.providerId);
if (removed) {
for (const session of currentSession.monitoringSessions.values()) {
if (session.providerId !== provider.providerId || (session.state !== 'active' && session.state !== 'degraded')) continue;
session.state = 'orphaned';
session.reason = 'Monitoring provider disappeared';
session.updatedAt = _now();
capabilities.emitEvent('audio-monitoring', 'monitoring-orphaned', _redactedMonitoringSession(session, _newPseudonymizer()));
}
}
_refreshMonitoringDuplicateSuppression();
const outcome = removed ? 'handled' : 'no-handler';
_recordOutcome({ domain: 'audio-monitoring', operation: 'unregister-provider', participantId: id, providerId: id, outcome });
if (removed) capabilities.emitEvent('audio-monitoring', 'provider-removed', { providerId: id, logicalMonitoringKey: provider.logicalMonitoringKey, removed: true });
_touch();
if (removed) return _handled({ providerId: id, logicalMonitoringKey: provider.logicalMonitoringKey, removed: true });
// `id` here is caller-supplied (no provider matched), so bound it before reflecting it back.
const safeId = _boundedReason(id);
return _noHandler(`Unknown monitoring provider: ${safeId}`, { providerId: safeId, removed: false });
}
function selectMonitoringProvider(query, requester = 'unknown') {
const payload = typeof query === 'string' ? { providerId: query } : _plainObject(query);
const provider = _findMonitoringProvider(payload);
// requestedKey is caller-supplied; bound it before echoing it back in the reason/payload.
const requestedKey = _boundedReason(_string(payload.logicalMonitoringKey || payload.logicalKey || payload.monitoringKey || payload.providerId || payload.id));
if (!provider || provider.supersededBy) {
const result = _unavailable(`Monitoring provider is unavailable: ${requestedKey}`, { logicalMonitoringKey: requestedKey, availability: 'unavailable' });
// No logicalSourceKey here: requestedKey is a monitoring key, not an audio-input logical
// source key, and reusing that field would misrepresent the outcome in diagnostics. The
// requested key is already present in the bounded reason.
_recordOutcome({ domain: 'audio-monitoring', operation: 'select-provider', participantId: requester, requesterId: requester, outcome: result.outcome, status: 'unavailable', reason: result.reason });
return result;
}
currentSession.selectedMonitoringProvider = _makeSelectedMonitoringProvider(provider, requester);
currentSession.storageStatus = _writeSelectedLogicalMonitoringKey(provider.logicalMonitoringKey);
_syncSelectedMonitoringProvider();
const summary = _summarySelectedMonitoringProvider(currentSession.selectedMonitoringProvider);
_recordOutcome({ domain: 'audio-monitoring', operation: 'select-provider', participantId: requester, requesterId: requester, providerId: provider.providerId, outcome: 'handled', status: provider.availability });
capabilities.emitEvent('audio-monitoring', 'provider-selected', summary);
_touch();
return _handled(summary);
}
function listMonitoringProviders(payload = {}) {
const includeUnavailable = payload.includeUnavailable !== false;
const providers = _visibleMonitoringProviders()
.filter(provider => includeUnavailable || provider.availability === 'available')
.map(_summaryMonitoringProvider);
_recordOutcome({ domain: 'audio-monitoring', operation: 'list-providers', participantId: OWNER_ID, outcome: 'handled', status: `${providers.length}` });
return _handled({ providers, selectedProvider: _summarySelectedMonitoringProvider(_syncSelectedMonitoringProvider()), timeoutMs: MONITORING_OPERATION_TIMEOUT_MS });
}
function _resolveMonitoringProvider(payload = {}) {
_refreshMonitoringDuplicateSuppression();
const explicit = _findMonitoringProvider(payload);
if (explicit && !explicit.supersededBy) return { outcome: 'handled', provider: explicit };
const selected = _syncSelectedMonitoringProvider();
if (selected && selected.providerId) {
const selectedProvider = currentSession.monitoringProviders.get(selected.providerId);
if (selectedProvider && !selectedProvider.supersededBy) return { outcome: 'handled', provider: selectedProvider };
}
const compatible = _visibleMonitoringProviders().filter(provider => provider.availability === 'available' || provider.availability === 'pending');
if (compatible.length === 1) return { outcome: 'handled', provider: compatible[0] };
if (compatible.length > 1) return { outcome: 'provider-selection-required', reason: 'Multiple compatible monitoring providers require an explicit provider choice', providers: compatible.map(_summaryMonitoringProvider) };
return { outcome: 'no-owner', reason: 'No monitoring provider is available', providers: [] };
}
async function _refreshMonitoringStatuses(payload = {}) {
const providers = _visibleMonitoringProviders().filter(provider => !payload.providerId || provider.providerId === payload.providerId);
// Prompt-free status must not hand the raw device sourceId to providers: selectedInput.sourceId is
// the raw input source id, so strip it (keep only the redaction-safe logicalSourceKey context).
const statusSourceRef = { ..._monitoringSourceRef(currentSession.selectedInput || {}), sourceId: '' };
// Refresh provider statuses concurrently: a single hung/slow provider must not serialize the
// whole prompt-free inspect path into O(N * timeout). Each provider mutates only its own record.
await Promise.allSettled(providers.map(async (provider) => {
if (provider.version !== 1) return;
const handler = provider.operationHandlers && provider.operationHandlers['monitoring.status'];
if (!provider.operations.includes('monitoring.status') || typeof handler !== 'function') return;
try {
const raw = await _withMonitoringTimeout(handler({ providerId: provider.providerId, sourceRef: statusSourceRef }), 'monitoring.status');
const result = _monitoringProviderOutcome(raw);
if (result.payload && typeof result.payload === 'object') {
// result.status is a clamped/derived value: a non-state provider outcome (e.g.
// 'no-handler') resolves to 'unknown', which is itself a valid availability and would
// wrongly overwrite a healthy provider. Only let an explicit payload.availability or a
// genuine reported state drive availability; a derived 'unknown' preserves the prior value.
const statusAvailability = result.status && result.status !== 'unknown' ? result.status : '';
provider.availability = _availability(result.payload.availability || statusAvailability || provider.availability, provider.availability);
provider.directMonitor = _normalizeDirectMonitor(result.payload.directMonitor || provider.directMonitor || {});
provider.latencySummary = _normalizeLatencySummary(result.payload.latencySummary || provider.latencySummary || {});
}
provider.reason = result.reason || provider.reason;
provider.lastChangedAt = _now();
_recordOutcome({ domain: 'audio-monitoring', operation: 'status', participantId: provider.providerId, providerId: provider.providerId, outcome: result.outcome, status: result.status, reason: result.reason });
} catch (err) {
provider.availability = 'failed';
provider.reason = err && err.timedOut ? 'Monitoring status timed out' : _boundedReason(err && err.message ? err.message : String(err));
_recordOutcome({ domain: 'audio-monitoring', operation: 'status', participantId: provider.providerId, providerId: provider.providerId, outcome: 'failed', status: err && err.timedOut ? 'timeout' : 'failed', reason: provider.reason });
}
}));
_syncSelectedMonitoringProvider();
// An explicit status refresh mutates provider state/outcomes, so bump diagnostics freshness.
_touch();
}
async function inspectMonitoring(payload = {}) {
if (payload.refreshStatus === true || payload.includeStatus === true) await _refreshMonitoringStatuses(payload);
const snapshotData = snapshot().domains['audio-monitoring'];
if (payload.providerId || payload.logicalMonitoringKey) {
const match = snapshotData.providers.find(provider => (payload.providerId && provider.providerId === payload.providerId) || (payload.logicalMonitoringKey && provider.logicalMonitoringKey === payload.logicalMonitoringKey));
return _handled({ ...snapshotData, provider: match || null });
}
return _handled(snapshotData);
}
async function _ensureMonitoringInput(payload, requesterId) {
const requiredChannelShape = _channelShape(payload.requiredChannelShape || payload.channelShape, 'unknown');
// Always resolve through audio-input open-source and derive the monitoring sourceRef from its
// redaction-safe payload — never the caller payload — so a caller can't bypass open-source
// readiness/ownership or inject arbitrary sourceId/logicalSourceKey fields that would leak into
// the shared session and other requesters' start() responses. openInputSource reuses an
// already-open session for the selected source, so this does not double-open; openSessionId is the
// only trusted cross-domain correlation.
return openInputSource({ ...(payload.sourceRef || {}), logicalSourceKey: payload.logicalSourceKey || payload.sourceRef?.logicalSourceKey, sourceId: payload.sourceId || payload.sourceRef?.sourceId, requesterId, purpose: payload.purpose, requiredChannelShape }, requesterId);
}
function _blockingInputOutcome(openResult) {
if (openResult.outcome === 'handled') return null;
const payload = _plainObject(openResult.payload);
const status = _string(payload.state || openResult.status, openResult.outcome);
if (openResult.outcome === 'denied') return { outcome: 'denied', state: 'denied', reason: openResult.reason || 'Input permission denied' };
if (status === 'incompatible') return { outcome: 'incompatible', state: 'failed', reason: openResult.reason || 'Selected source cannot satisfy requested channel shape' };
if (openResult.outcome === 'no-owner') return { outcome: 'no-owner', state: 'unavailable', reason: openResult.reason || 'No selected input source is available' };
if (openResult.outcome === 'failed') return { outcome: 'failed', state: 'failed', reason: openResult.reason || 'Input source failed to open' };
return { outcome: 'unavailable', state: 'unavailable', reason: openResult.reason || 'Input source is unavailable' };
}
async function startMonitoring(spec = {}, requester = 'unknown') {
const payload = _plainObject(spec);
// Identity comes from the authenticated dispatch caller, never the payload — a
// payload-supplied requesterId/source could otherwise spoof another requester and
// misattribute or share a monitoring session it does not own (mirrors openInputSource).
const requesterId = _requesterId(requester, 'unknown');
const requiredChannelShape = _channelShape(payload.requiredChannelShape || payload.channelShape, 'unknown');
const directMonitorPreference = payload.directMonitorPreference === 'muted' || payload.directMonitorPreference === 'unmuted' ? payload.directMonitorPreference : _readDirectMonitorPreference();
const authorization = _authorizationMode(payload.authorization || payload.mode);
capabilities.emitEvent('audio-monitoring', 'monitoring-start-requested', { requesterId, authorization, requiredChannelShape });
const resolved = _resolveMonitoringProvider(payload);
if (resolved.outcome !== 'handled') {
const result = _monitoringResultFor(resolved.outcome, resolved.reason, { providers: resolved.providers || [] });
_recordOutcome({ domain: 'audio-monitoring', operation: 'start', participantId: requesterId, requesterId, outcome: result.outcome, status: resolved.outcome, reason: result.reason });
if (result.outcome === 'provider-selection-required') capabilities.emitEvent('audio-monitoring', 'provider-selection-required', result.payload);
return result;
}
const provider = resolved.provider;
const selectedRef = _monitoringSourceRef(payload.sourceRef || currentSession.selectedInput || {});
const existing = _findCompatibleMonitoringSession(provider, selectedRef, requiredChannelShape, directMonitorPreference);
if (existing) {
// Attaching to a shared session must still honor this requester's direct-monitor requirement:
// if it conflicts with the active preference, surface degraded (or unsupported-command when
// strict) and annotate the requester ref, mirroring the fresh-start conflict handling.
const attachRequirement = _directMonitorState(payload.directMonitorRequirement, 'unknown');
const attachConflict = (attachRequirement === 'muted' || attachRequirement === 'unmuted') && attachRequirement !== directMonitorPreference;
const attachOutcome = attachConflict ? (payload.directMonitorStrict === true ? 'unsupported-command' : 'degraded') : 'handled';
const conflictReason = 'Direct-monitor requirement conflicts with user/default setting';
if (!existing.requesters.some(item => item.requesterId === requesterId)) existing.requesters.push(_monitoringRequesterRef({ ...payload, requesterId, requiredChannelShape, directMonitorRequirement: attachRequirement, status: attachConflict ? attachOutcome : '', reason: attachConflict ? conflictReason : '' }));
existing.lastUsedAt = _now();
_recordOutcome({ domain: 'audio-monitoring', operation: 'start', participantId: requesterId, requesterId, providerId: provider.providerId, monitoringId: existing.monitoringId, outcome: attachOutcome, status: existing.state, reason: attachConflict ? conflictReason : '' });
if (attachConflict) _emitMonitoringOutcome(attachOutcome, existing);
_touch();
return _monitoringResultFor(attachOutcome, attachConflict ? conflictReason : '', _clone(existing));
}
if (authorization !== 'user-action') {
const result = _userActionRequired('Fresh monitoring start requires explicit user action', { requesterId, providerId: provider.providerId });
_recordOutcome({ domain: 'audio-monitoring', operation: 'start', participantId: requesterId, requesterId, providerId: provider.providerId, outcome: result.outcome, status: 'user-action-required', reason: result.reason });
// user-action-required is not a permission denial — emit a dedicated event (like
// provider-selection-required) so subscribers don't conflate it with 'monitoring-denied'.
capabilities.emitEvent('audio-monitoring', 'monitoring-user-action-required', result.payload);
return result;
}
if (provider.availability !== 'available' && provider.availability !== 'pending') {
const result = _unavailable(provider.reason || `Monitoring provider is ${provider.availability}`, _summaryMonitoringProvider(provider));
_recordOutcome({ domain: 'audio-monitoring', operation: 'start', participantId: requesterId, requesterId, providerId: provider.providerId, outcome: result.outcome, status: provider.availability, reason: result.reason });
capabilities.emitEvent('audio-monitoring', 'monitoring-unavailable', result.payload);
return result;
}
if (provider.version !== 1) {
const result = _incompatibleVersion('Monitoring provider requires audio-session contract version 1', _summaryMonitoringProvider(provider));
_recordOutcome({ domain: 'audio-monitoring', operation: 'start', participantId: requesterId, requesterId, providerId: provider.providerId, outcome: result.outcome, status: 'incompatible-version', reason: result.reason });
return result;
}
const openResult = await _ensureMonitoringInput(payload, requesterId);
const blockingInput = _blockingInputOutcome(openResult);
const sourceRef = _monitoringSourceRef(openResult.payload || payload.sourceRef || currentSession.selectedInput || {});
if (blockingInput) {
const session = {
monitoringId: _string(payload.monitoringId || payload.id, _id('monitoring')),
sessionKey: _monitoringSessionKey(provider, sourceRef, requiredChannelShape, directMonitorPreference),
providerId: provider.providerId,
logicalMonitoringKey: provider.logicalMonitoringKey,
sourceRef,
openInputSessionId: sourceRef.openSessionId || '',
state: blockingInput.state,
requesters: [_monitoringRequesterRef({ ...payload, requesterId, requiredChannelShape, status: blockingInput.outcome, reason: blockingInput.reason })],
directMonitor: _normalizeDirectMonitor({ ...provider.directMonitor, preference: directMonitorPreference }),
latencySummary: _clone(provider.latencySummary),
reason: blockingInput.reason,
startedAt: '',
lastUsedAt: _now(),
stoppedAt: '',
updatedAt: _now(),
};
currentSession.monitoringSessions.set(session.monitoringId, session);
_recordOutcome({ domain: 'audio-monitoring', operation: 'start', participantId: requesterId, requesterId, providerId: provider.providerId, monitoringId: session.monitoringId, logicalSourceKey: sourceRef.logicalSourceKey, sourceId: sourceRef.sourceId, openSessionId: sourceRef.openSessionId, outcome: blockingInput.outcome, status: session.state, reason: blockingInput.reason });
_emitMonitoringOutcome(blockingInput.outcome, session);
_touch();
return _monitoringResultFor(blockingInput.outcome, blockingInput.reason, _clone(session));
}
if (!provider.operations.includes('monitoring.start')) {
const result = _unsupportedCommand('Monitoring provider does not support monitoring.start', _summaryMonitoringProvider(provider));
_recordOutcome({ domain: 'audio-monitoring', operation: 'start', participantId: requesterId, requesterId, providerId: provider.providerId, outcome: result.outcome, status: 'unsupported-command', reason: result.reason });
return result;
}
const handler = provider.operationHandlers && provider.operationHandlers['monitoring.start'];
if (typeof handler !== 'function') {
const result = _noHandler('Monitoring provider has no monitoring.start handler', _summaryMonitoringProvider(provider));
_recordOutcome({ domain: 'audio-monitoring', operation: 'start', participantId: requesterId, requesterId, providerId: provider.providerId, outcome: result.outcome, status: 'no-handler', reason: result.reason });
return result;
}
const monitoringId = _string(payload.monitoringId || payload.id, _id('monitoring'));
const directMonitorRequirement = _directMonitorState(payload.directMonitorRequirement, payload.directMonitorRequirement ? 'unknown' : 'unknown');
const conflict = (directMonitorRequirement === 'muted' || directMonitorRequirement === 'unmuted') && directMonitorRequirement !== directMonitorPreference;
try {
const raw = await _withMonitoringTimeout(handler({ monitoringId, providerId: provider.providerId, requesterId, sourceRef, requiredChannelShape, directMonitorPreference, directMonitorRequirement }), 'monitoring.start');
const providerResult = _monitoringProviderOutcome(raw, {}, true);
// A provider that reports a terminal state (denied/unavailable/failed/stopped/orphaned) but a
// 'handled' (or omitted) outcome must not be coerced into an active session — derive the
// effective outcome from the reported state so the real condition surfaces. 'degraded' is
// intentionally NOT promoted: it stays a handled start with session.state 'degraded'
// (a working-but-degraded session), matching the established degraded contract.
const reportedState = providerResult.status;
const TERMINAL_MONITORING_STATES = ['denied', 'unavailable', 'failed', 'stopped', 'orphaned'];
const effectiveOutcome = providerResult.outcome === 'handled' && TERMINAL_MONITORING_STATES.includes(reportedState)
? (reportedState === 'orphaned' ? 'failed' : reportedState)
: providerResult.outcome;
const baseState = effectiveOutcome === 'handled' ? (reportedState === 'degraded' ? 'degraded' : 'active') : _monitoringState(reportedState, effectiveOutcome === 'degraded' ? 'degraded' : effectiveOutcome);
// The direct-monitor conflict only downgrades a session that the provider otherwise
// accepted (handled/degraded). When the provider itself reported a terminal state the
// real provider reason must win, so the conflict status/reason must NOT overwrite it.
const conflictApplies = conflict && (effectiveOutcome === 'handled' || effectiveOutcome === 'degraded');
const resultOutcome = conflictApplies ? (payload.directMonitorStrict === true ? 'unsupported-command' : 'degraded') : effectiveOutcome;
const sessionState = conflictApplies && baseState === 'active' ? 'degraded' : baseState;
const session = {
monitoringId,
sessionKey: _monitoringSessionKey(provider, sourceRef, requiredChannelShape, directMonitorPreference),
providerId: provider.providerId,
logicalMonitoringKey: provider.logicalMonitoringKey,
sourceRef,
openInputSessionId: sourceRef.openSessionId || '',
state: sessionState,
requesters: [_monitoringRequesterRef({ ...payload, requesterId, requiredChannelShape, directMonitorRequirement, status: conflictApplies ? resultOutcome : '', reason: conflictApplies ? 'Direct-monitor requirement conflicts with user/default setting' : '' })],
directMonitor: _normalizeDirectMonitor({ ...((providerResult.payload && providerResult.payload.directMonitor) || provider.directMonitor || {}), preference: directMonitorPreference }),
latencySummary: _normalizeLatencySummary((providerResult.payload && providerResult.payload.latencySummary) || provider.latencySummary || {}),
reason: conflictApplies ? 'Direct-monitor requirement conflicts with user/default setting' : providerResult.reason,
startedAt: sessionState === 'active' || sessionState === 'degraded' ? _now() : '',
lastUsedAt: _now(),
stoppedAt: '',
updatedAt: _now(),
};
currentSession.monitoringSessions.set(monitoringId, session);
provider.directMonitor = _normalizeDirectMonitor({ ...provider.directMonitor, ...session.directMonitor, preference: directMonitorPreference });
provider.latencySummary = session.latencySummary;
_recordOutcome({ domain: 'audio-monitoring', operation: 'start', participantId: requesterId, requesterId, providerId: provider.providerId, monitoringId, logicalSourceKey: sourceRef.logicalSourceKey, sourceId: sourceRef.sourceId, openSessionId: sourceRef.openSessionId, outcome: resultOutcome, status: session.state, reason: session.reason || providerResult.reason });
_emitMonitoringOutcome(resultOutcome, session);
_touch();
return _monitoringResultFor(resultOutcome, session.reason || providerResult.reason || `Monitoring returned ${resultOutcome}`, _clone(session));
} catch (err) {
const reason = err && err.timedOut ? 'Monitoring start timed out' : _boundedReason(err && err.message ? err.message : String(err));
const session = { monitoringId, sessionKey: _monitoringSessionKey(provider, sourceRef, requiredChannelShape, directMonitorPreference), providerId: provider.providerId, logicalMonitoringKey: provider.logicalMonitoringKey, sourceRef, openInputSessionId: sourceRef.openSessionId || '', state: 'failed', requesters: [_monitoringRequesterRef({ ...payload, requesterId, requiredChannelShape, status: 'failed', reason })], directMonitor: _normalizeDirectMonitor({ ...provider.directMonitor, preference: directMonitorPreference }), latencySummary: _clone(provider.latencySummary), reason, startedAt: '', lastUsedAt: _now(), stoppedAt: '', updatedAt: _now() };
currentSession.monitoringSessions.set(monitoringId, session);
_recordOutcome({ domain: 'audio-monitoring', operation: 'start', participantId: requesterId, requesterId, providerId: provider.providerId, monitoringId, outcome: 'failed', status: err && err.timedOut ? 'timeout' : 'failed', reason });
_emitMonitoringOutcome('failed', session);
_touch();
return _failed(reason, { ..._clone(session), timedOut: !!(err && err.timedOut) });
}
}
async function stopMonitoring(target, requester = 'unknown') {
const payload = typeof target === 'object' ? _plainObject(target) : { monitoringId: target };
const requesterId = _requesterId(requester, requester);
const id = _string(payload.monitoringId || payload.id);
// stopAll is a user-authoritative global stop (see the audio-monitoring control-plane contract).
// Require an explicit user action so a background requester can't tear down everyone's monitoring
// (a DoS); targeted stops remain ownership-scoped below.
if (payload.stopAll && payload.authorization !== 'user-action') {
const result = _userActionRequired('Stopping all monitoring sessions requires explicit user action', { requesterId });
_recordOutcome({ domain: 'audio-monitoring', operation: 'stop', participantId: requesterId, requesterId, outcome: result.outcome, status: 'user-action-required', reason: result.reason });
return result;
}
const sessions = payload.stopAll ? Array.from(currentSession.monitoringSessions.values()).filter(session => session.state === 'active' || session.state === 'degraded') : [currentSession.monitoringSessions.get(id)].filter(Boolean);
if (!sessions.length) {
// `id` is caller-supplied and didn't match a session; bound it before reflecting it back.
const safeId = _boundedReason(id);
const result = _noHandler(`Unknown monitoring session: ${safeId || 'active session'}`, { monitoringId: safeId });
_recordOutcome({ domain: 'audio-monitoring', operation: 'stop', participantId: requesterId, requesterId, monitoringId: safeId, outcome: result.outcome, reason: result.reason });
return result;
}
let lastResult = null;
for (const session of sessions) {
// A targeted stop may only release/tear down a session the caller actually owns —
// otherwise any requester could end another's monitoring. `stopAll` is the documented
// explicit user-authoritative global stop (see audio-monitoring-control-plane contract)
// and is intentionally not ownership-scoped. An already-ownerless session (every requester
// released, or a failed/timed-out final stop that emptied requesters before the provider
// confirmed) has no owner left to protect, so any caller may retry the stop on it.
if (!payload.stopAll && session.requesters.length > 0 && !session.requesters.some(item => item.requesterId === requesterId)) {
lastResult = _noHandler('Requester does not own this monitoring session', { monitoringId: session.monitoringId });
_recordOutcome({ domain: 'audio-monitoring', operation: 'stop', participantId: requesterId, requesterId, providerId: session.providerId, monitoringId: session.monitoringId, outcome: lastResult.outcome, status: 'not-owner', reason: lastResult.reason });
continue;
}
const beforeCount = session.requesters.length;
if (!payload.stopAll) session.requesters = session.requesters.filter(item => item.requesterId !== requesterId);
if (!payload.stopAll && beforeCount > 1 && session.requesters.length > 0) {
session.lastUsedAt = _now();
_recordOutcome({ domain: 'audio-monitoring', operation: 'stop', participantId: requesterId, requesterId, providerId: session.providerId, monitoringId: session.monitoringId, outcome: 'handled', status: session.state });
lastResult = _handled(_clone(session));
continue;
}
const provider = currentSession.monitoringProviders.get(session.providerId) || knownMonitoringProviders.get(session.providerId);
const handler = provider && provider.operationHandlers && provider.operationHandlers['monitoring.stop'];
try {
if (!provider) {
// Provider disappeared — we can't confirm the live capture actually stopped, so orphan
// the session and report no-owner rather than falsely claiming 'stopped'.
session.state = 'orphaned';
session.reason = 'Monitoring provider is no longer available';
session.updatedAt = _now();
_recordOutcome({ domain: 'audio-monitoring', operation: 'stop', participantId: requesterId, requesterId, providerId: session.providerId, monitoringId: session.monitoringId, outcome: 'no-owner', status: 'orphaned', reason: session.reason });
capabilities.emitEvent('audio-monitoring', 'monitoring-orphaned', _redactedMonitoringSession(session, _newPseudonymizer()));
lastResult = _noOwner(session.reason, _clone(session));
continue;
}
if (!provider.operations.includes('monitoring.stop')) {
// Provider advertises no stop operation — don't pretend the live capture stopped.
session.state = 'failed';
session.reason = 'Monitoring provider does not support monitoring.stop';
session.updatedAt = _now();
_recordOutcome({ domain: 'audio-monitoring', operation: 'stop', participantId: requesterId, requesterId, providerId: session.providerId, monitoringId: session.monitoringId, outcome: 'unsupported-command', status: 'unsupported-command', reason: session.reason });
lastResult = _unsupportedCommand(session.reason, _clone(session));
continue;
}
if (typeof handler === 'function') {
const raw = await _withMonitoringTimeout(handler({ monitoringId: session.monitoringId, providerId: session.providerId, requesterIds: Array.from(new Set(session.requesters.map(item => item.requesterId).concat(requesterId))), reason: payload.stopAll ? 'user-stop-all' : 'final-requester-released' }), 'monitoring.stop');
const providerResult = _monitoringProviderOutcome(raw, _clone(session));
// A 'handled'/'stopped' outcome paired with a terminal non-stopped status (failed/
// denied/unavailable/orphaned) must not be reported as a clean stop — derive the
// effective outcome from the reported state, mirroring startMonitoring.
const reportedState = providerResult.status;
const stopEffectiveOutcome = (providerResult.outcome === 'handled' || providerResult.outcome === 'stopped') && ['denied', 'unavailable', 'failed', 'orphaned'].includes(reportedState)
? (reportedState === 'orphaned' ? 'failed' : reportedState)
: providerResult.outcome;
if (stopEffectiveOutcome !== 'handled' && stopEffectiveOutcome !== 'stopped') {
session.state = stopEffectiveOutcome === 'failed' ? 'failed' : _monitoringState(reportedState, stopEffectiveOutcome);
session.reason = providerResult.reason;
session.updatedAt = _now();
_recordOutcome({ domain: 'audio-monitoring', operation: 'stop', participantId: requesterId, requesterId, providerId: session.providerId, monitoringId: session.monitoringId, outcome: stopEffectiveOutcome, status: session.state, reason: providerResult.reason });
_emitMonitoringOutcome(stopEffectiveOutcome, session);
lastResult = _monitoringResultFor(stopEffectiveOutcome, providerResult.reason || `Monitoring stop returned ${stopEffectiveOutcome}`, _clone(session));
continue;
}
} else {
// Provider advertises monitoring.stop but supplied no handler.
session.state = 'failed';
session.reason = 'Monitoring provider has no monitoring.stop handler';
_recordOutcome({ domain: 'audio-monitoring', operation: 'stop', participantId: requesterId, requesterId, providerId: session.providerId, monitoringId: session.monitoringId, outcome: 'no-handler', status: 'no-handler', reason: session.reason });
lastResult = _noHandler(session.reason, _clone(session));
continue;
}
session.state = 'stopped';
session.stoppedAt = _now();
session.updatedAt = _now();
session.requesters = [];
_recordOutcome({ domain: 'audio-monitoring', operation: 'stop', participantId: requesterId, requesterId, providerId: session.providerId, monitoringId: session.monitoringId, outcome: 'stopped', status: 'stopped' });
capabilities.emitEvent('audio-monitoring', 'monitoring-stopped', _redactedMonitoringSession(session, _newPseudonymizer()));
lastResult = _stopped('Monitoring stopped', _clone(session));
} catch (err) {
session.state = 'failed';
session.reason = err && err.timedOut ? 'Monitoring stop timed out' : _boundedReason(err && err.message ? err.message : String(err));
session.updatedAt = _now();
_recordOutcome({ domain: 'audio-monitoring', operation: 'stop', participantId: requesterId, requesterId, providerId: session.providerId, monitoringId: session.monitoringId, outcome: 'failed', status: err && err.timedOut ? 'timeout' : 'failed', reason: session.reason });
_emitMonitoringOutcome('failed', session);
lastResult = _failed(session.reason, { ..._clone(session), timedOut: !!(err && err.timedOut) });
}
}
_touch();
return lastResult || _stopped('Monitoring stopped', {});
}
async function setDirectMonitoringState(payload = {}, requester = 'unknown') {
const source = typeof payload === 'string' ? { state: payload } : _plainObject(payload);
const state = _directMonitorState(source.state, 'unknown');
// Identity comes from the authenticated dispatch caller, never the payload (anti-spoofing).
const requesterId = _requesterId(requester, 'unknown');
if (state !== 'muted' && state !== 'unmuted') {
const result = _denied('Direct-monitor preference must be muted or unmuted', { state });
_recordOutcome({ domain: 'audio-monitoring', operation: 'set-direct-monitor', participantId: requesterId, requesterId, outcome: result.outcome, status: 'invalid-state', reason: result.reason });
return result;
}
currentSession.directMonitor = _normalizeDirectMonitor({ ...currentSession.directMonitor, state, preference: state, applied: false, lastChangedAt: _now() });
currentSession.storageStatus = _writeDirectMonitorPreference(state);
const activeSessions = Array.from(currentSession.monitoringSessions.values()).filter(session => session.state === 'active' || session.state === 'degraded');
let outcome = 'handled';
let reason = '';
for (const session of activeSessions) {
const provider = currentSession.monitoringProviders.get(session.providerId);
const handler = provider && provider.operationHandlers && (provider.operationHandlers['monitoring.set-direct-monitor'] || provider.operationHandlers['direct-monitor.set']);
const supports = provider && (provider.operations.includes('monitoring.set-direct-monitor') || provider.operations.includes('direct-monitor.set'));
if (supports && typeof handler === 'function') {
try {
const raw = await _withMonitoringTimeout(handler({ monitoringId: session.monitoringId, providerId: session.providerId, state, requesterId }), 'set-direct-monitor');
const result = _monitoringProviderOutcome(raw, session.directMonitor);
if (result.outcome !== 'handled' && result.outcome !== 'degraded') {
outcome = result.outcome;
reason = result.reason || reason;
}
// Only claim applied when the provider explicitly reports a directMonitor summary; a
// bare 'handled' doesn't confirm the hardware preference was actually applied.
session.directMonitor = _normalizeDirectMonitor((result.payload && result.payload.directMonitor) || { state, control: 'supported', preference: state, applied: false, reason: result.reason, lastChangedAt: _now() });
} catch (err) {
outcome = 'failed';
reason = err && err.timedOut ? 'Direct-monitor apply timed out' : _boundedReason(err && err.message ? err.message : String(err));
session.directMonitor = _normalizeDirectMonitor({ state, control: 'unknown', preference: state, applied: false, reason, lastChangedAt: _now() });
}
} else {
outcome = outcome === 'handled' ? 'unsupported-command' : outcome;
reason = reason || 'Monitoring provider does not support direct-monitor control';
session.directMonitor = _normalizeDirectMonitor({ state, control: 'unsupported', preference: state, applied: false, reason, lastChangedAt: _now() });
}
// The session key encodes the direct-monitor policy; refresh its policy segment to the new
// user preference so a later start with this preference re-attaches to the live session
// instead of failing to match and forking a duplicate (or returning user-action-required).
session.sessionKey = String(session.sessionKey).split('::').slice(0, -1).concat(state).join('::');
session.updatedAt = _now();
}
// Reflect the actual per-session apply results back into the domain-level summary so
// snapshot().domains['audio-monitoring'].directMonitor and the emitted event are not stale
// (the pre-call seed above optimistically set applied:false/unknown control). Derive from the
// sessions' own directMonitor states — not the aggregate outcome — so a provider that returns
// outcome:'handled' but directMonitor.applied:false is reported as not-applied.
if (activeSessions.length) {
const monitors = activeSessions.map(session => session.directMonitor || {});
const applied = monitors.every(monitor => monitor.applied === true);
const control = monitors.some(monitor => monitor.control === 'unsupported') ? 'unsupported'
: monitors.some(monitor => monitor.control === 'unavailable') ? 'unavailable'
: (monitors.every(monitor => monitor.control === 'supported') ? 'supported' : 'unknown');
// Prefer the command-level error reason (unsupported/failed/timeout), else surface the first
// per-session reason so a handled-but-not-applied provider note (e.g. 'hardware busy') isn't lost.
const summaryReason = reason || monitors.map(monitor => monitor.reason).find(value => value) || '';
currentSession.directMonitor = _normalizeDirectMonitor({ ...currentSession.directMonitor, state, preference: state, control, applied, reason: summaryReason, lastChangedAt: _now() });
}
const summary = { directMonitor: _clone(currentSession.directMonitor), sessions: activeSessions.map(_clone) };
_recordOutcome({ domain: 'audio-monitoring', operation: 'set-direct-monitor', participantId: requesterId, requesterId, outcome, status: state, reason });
// The event is broadcast to all observers, so emit redaction-safe sessions; the raw summary is
// only returned to the (owning) dispatch caller.
const eventPseudonymizer = _newPseudonymizer();
capabilities.emitEvent('audio-monitoring', 'direct-monitor-changed', { directMonitor: _clone(currentSession.directMonitor), sessions: activeSessions.map(session => _redactedMonitoringSession(session, eventPseudonymizer)) });
_touch();
return _monitoringResultFor(outcome, reason || 'Direct-monitor preference updated', summary);
}
function registerStemOwner(owner) {
const source = _plainObject(owner);
const previousOwnerId = currentSession.stemOwner ? currentSession.stemOwner.ownerId : '';
if (currentSession.stemOwner && source.availability && source.availability !== 'available') {
for (const claim of currentSession.stemClaims.values()) {
if (claim.state === 'active') {
claim.state = 'orphaned';
claim.updatedAt = _now();
capabilities.emitEvent('stems', 'claim-orphaned', _clone(claim));
}
}
}
currentSession.stemOwner = {
ownerId: _string(source.ownerId || source.participantId || source.id, 'stems'),
stemIds: _strings(source.stemIds),
stemStates: _clone(source.stemStates || {}) || {},
availability: _string(source.availability, 'available'),
};
// When the provider identity changes, retire the prior provider participant
// so the stems pipeline/diagnostics don't keep a stale active provider.
// Never retire the audio-session coordinator (OWNER_ID) itself.
if (previousOwnerId && previousOwnerId !== currentSession.stemOwner.ownerId && previousOwnerId !== OWNER_ID && typeof capabilities.unregisterParticipant === 'function') {
capabilities.unregisterParticipant(previousOwnerId, 'stems');
}
if (typeof capabilities.registerParticipant === 'function') {
capabilities.registerParticipant(currentSession.stemOwner.ownerId, {
stems: {
roles: ['provider'],
operations: ['stem.get-state', 'stem.apply-automation', 'stem.restore-automation'],
events: ['owner-available', 'owner-unavailable'],
kind: 'provider-coordinator',
mode: currentSession.stemOwner.availability === 'available' ? 'active' : 'disabled',
compatibility: 'none',
ownership: 'multi-provider',
safety: 'safe',
runtime: true,
version: 1,
description: 'Active Stems provider registered behind the audio-session coordinator.',
},
});
}
_recordOutcome({ domain: 'stems', operation: 'owner.register', participantId: currentSession.stemOwner.ownerId, outcome: 'handled', status: currentSession.stemOwner.availability });
const ownerEvent = currentSession.stemOwner.availability === 'available' ? 'owner-available' : 'owner-unavailable';
capabilities.emitEvent('stems', ownerEvent, _clone(currentSession.stemOwner));
_touch();
return _handled(_clone(currentSession.stemOwner));
}
function _pruneStemClaims() {
// Bound claim growth for long-running/buggy sessions so snapshots and
// diagnostics bundles stay small. Drop the oldest terminal-state claims
// first (Map preserves insertion order); never drop an active claim.
if (currentSession.stemClaims.size <= MAX_DOMAIN_ITEMS) return;
for (const [claimId, claim] of currentSession.stemClaims) {
if (currentSession.stemClaims.size <= MAX_DOMAIN_ITEMS) break;
if (claim.state !== 'active') currentSession.stemClaims.delete(claimId);
}
}
function muteStems(spec = {}) {
if (!currentSession.stemOwner || currentSession.stemOwner.availability !== 'available') {
const result = _noOwner('No active stem owner is available');
_recordOutcome({ domain: 'stems', operation: 'mute', participantId: _string(spec.requester, ''), outcome: result.outcome, status: currentSession.stemOwner ? currentSession.stemOwner.availability : 'no-owner', reason: result.reason });
return result;
}
const source = _plainObject(spec);
const claimId = _string(source.claimId, _id('stem-claim'));
const claim = {
claimId,
requesterId: _string(source.requester || source.requesterId, 'unknown'),
targetStemIds: _strings(source.stemIds || source.targetStemIds),
requestedAction: _string(source.action || source.requestedAction, 'mute'),
restoreSnapshot: _clone(source.restoreSnapshot || currentSession.stemOwner.stemStates) || {},
state: 'active',
createdAt: _now(),
updatedAt: _now(),
};
currentSession.stemClaims.set(claimId, claim);
_pruneStemClaims();
capabilities.claim({ capability: 'stems', claimId, requester: claim.requesterId, owner: currentSession.stemOwner.ownerId, target: { stemIds: claim.targetStemIds } });
_recordOutcome({ domain: 'stems', operation: 'mute', participantId: claim.requesterId, outcome: 'handled' });
capabilities.emitEvent('stems', 'automation-applied', _clone(claim));
_touch();
return _handled(_clone(claim));
}
function restoreStems(spec = {}) {
const source = _plainObject(spec);
const claimId = _string(source.claimId || source.id);
const claim = currentSession.stemClaims.get(claimId);
if (!claim) {
const result = _noHandler(`Unknown stem automation claim: ${claimId}`);
_recordOutcome({ domain: 'stems', operation: 'restore', participantId: source.requester, outcome: result.outcome, reason: result.reason });
return result;
}
claim.state = source.overridden || claim.state === 'overridden' ? 'overridden' : 'restored';
claim.updatedAt = _now();
capabilities.release({ capability: 'stems', claimId, requester: source.requester || claim.requesterId });
_recordOutcome({ domain: 'stems', operation: 'restore', participantId: source.requester || claim.requesterId, outcome: claim.state === 'overridden' ? 'overridden' : 'handled' });
capabilities.emitEvent('stems', claim.state === 'overridden' ? 'automation-overridden' : 'automation-restored', _clone(claim));
_touch();
return claim.state === 'overridden' ? { outcome: 'overridden', payload: _clone(claim) } : _handled(_clone(claim));
}
function recordStemManualOverride(override = {}) {
const source = _plainObject(override);
const targets = _strings(source.stemIds || source.targetStemIds);
const overriddenClaims = [];
for (const claim of currentSession.stemClaims.values()) {
if (claim.state !== 'active') continue;
const claimTargets = Array.isArray(claim.targetStemIds) ? claim.targetStemIds : [];
const matches = !targets.length || !claimTargets.length || claimTargets.some(stemId => targets.includes(stemId));
if (!matches) continue;
claim.state = 'overridden';
claim.updatedAt = _now();
overriddenClaims.push(_clone(claim));
capabilities.emitEvent('stems', 'automation-overridden', _clone(claim));
}
if (typeof capabilities.recordUserOverride === 'function') {
capabilities.recordUserOverride({ capability: 'stems', source: source.requester || 'user', target: { stemIds: targets }, reason: source.reason || 'Manual stem choice overrode automation' });
}
_recordOutcome({ domain: 'stems', operation: 'manual-override', participantId: source.requester || 'user', outcome: overriddenClaims.length ? 'overridden' : 'handled', status: overriddenClaims.length ? 'overridden' : 'no-active-claim', reason: source.reason });
_touch();
return { overriddenClaims };
}
function recordBridgeHit(bridge = {}) {
const source = _plainObject(bridge);
const domain = DOMAINS.includes(source.domain) ? source.domain : _string(source.capability, 'audio-mix');
// Bridge entries are exported verbatim in snapshot().domains[].bridges, so bound the
// caller-provided string fields (a legacy caller could pass a path/token-like value).
const legacySurface = _boundedReason(source.legacySurface) || 'unknown';
// Preserve the original bridgeId fallback key (`${domain}.legacy`, not `.unknown`) for stability.
const bridgeId = _boundedReason(source.bridgeId || source.shimId) || `${domain}.${_boundedReason(source.legacySurface) || 'legacy'}`;
const entry = {
bridgeId,
legacySurface,
domain,
participantId: _boundedReason(source.participantId || source.source || source.requester) || 'legacy-runtime',
logicalSourceKey: _boundedReason(source.logicalSourceKey || source.logicalKey || source.sourceKey),
outcome: _string(source.outcome, 'handled'),
status: _string(source.status, 'used'),
reason: _boundedReason(source.reason),
hitCount: (currentSession.bridges.get(bridgeId)?.hitCount || 0) + 1,
lastHitAt: _now(),
};
currentSession.bridges.set(bridgeId, entry);
if (typeof capabilities.recordLegacyHit === 'function') {
capabilities.recordLegacyHit({ capability: domain, legacySurface: entry.legacySurface, source: entry.participantId, reason: entry.reason, shimId: bridgeId });
}
_recordOutcome({ domain, operation: 'bridge-hit', participantId: entry.participantId, bridgeId, outcome: entry.outcome, status: entry.status, reason: entry.reason });
capabilities.emitEvent(domain, 'bridge-hit', _clone(entry));
_touch();
return _clone(entry);
}
function _newPseudonymizer() {
const seen = new Map();
let count = 0;
return function pseudonym(value, prefix = 'source') {
const key = `${prefix}:${_string(value, prefix)}`;
if (!seen.has(key)) {
count += 1;
seen.set(key, `${prefix}-${String(count).padStart(2, '0')}`);
}
return seen.get(key);
};
}
function _redactedSource(source, pseudonymize) {
// Per-bundle pseudonym (spec FR-011/SC-005: scoped to one export, not stable
// across exports). Keyed off the unique sourceId — used only as an internal
// pseudonymizer key, never exposed — so distinct sources stay distinct even
// when they share a device label, and monitoring (also keyed off sourceId)
// correlates within the bundle.
const sourcePseudonym = pseudonymize(source.sourceId, 'source');
return {
sourceId: sourcePseudonym,
logicalSourceKey: source.logicalSourceKey,
providerId: source.providerId,
ownerPluginId: source.ownerPluginId,
kind: source.kind,
label: source.label || sourcePseudonym,
channelSummary: _clone(source.channelSummary),
channelCount: source.channelCount,
availability: source.availability,
selected: !!source.selected,
sourceMode: source.sourceMode,
compatibilitySource: source.compatibilitySource || '',
supersededBy: source.supersededBy ? pseudonymize(source.supersededBy, 'source') : '',
reason: source.reason || '',
lastSeenAt: source.lastSeenAt || '',
diagnosticsPseudonym: sourcePseudonym,
};
}
function _redactedSelectedInput(selected, pseudonymize) {
if (!selected) return null;
return {
logicalSourceKey: selected.logicalSourceKey,
sourceId: selected.sourceId ? pseudonymize(selected.sourceId, 'source') : '',
providerId: selected.providerId || '',
availability: selected.availability || 'unavailable',
restored: !!selected.restored,
restoreStatus: selected.restoreStatus || 'not-selected',
selectedAt: selected.selectedAt || selected.lastSelectedAt || '',
lastSelectedAt: selected.lastSelectedAt || '',
lastRestoredAt: selected.lastRestoredAt || '',
requesterId: selected.requesterId || '',
};
}
function _inputSessionSummary(spec, pseudonymize) {
const source = spec.source || {};
const openSessionId = spec.openSessionId || _id('input-open');
return {
openSessionId,
logicalSourceKey: source.logicalSourceKey || spec.logicalSourceKey || '',
sourceId: source.sourceId ? pseudonymize(source.sourceId, 'source') : '',
providerId: source.providerId || spec.providerId || '',
channelShape: _channelShape(spec.channelShape, 'unknown'),
state: _string(spec.state, 'open'),
// Match _redactedOpenSession's requester shape ({ requesterId, purpose }) — no per-requester
// openedAt — so degraded session summaries and real open sessions share one schema.
requesters: [{ requesterId: _requesterId(spec.requesterId), purpose: _boundedReason(spec.purpose).slice(0, 80) }].filter(item => item.requesterId),
reason: _boundedReason(spec.reason),
};
}
function _redactedOpenSession(session, pseudonymize) {
if (!session) return null;
return {
openSessionId: session.openSessionId,
logicalSourceKey: session.logicalSourceKey,
sourceId: session.sourceId ? pseudonymize(session.sourceId, 'source') : '',
providerId: session.providerId,
channelShape: session.channelShape,
state: session.state,
requesters: Array.isArray(session.requesters) ? session.requesters.map(requester => ({ requesterId: requester.requesterId, purpose: requester.purpose || '' })) : [],
openedAt: session.openedAt || '',
lastUsedAt: session.lastUsedAt || '',
closedAt: session.closedAt || '',
reason: session.reason || '',
};
}
function _redactedMonitoringProvider(provider) {
// supersededBy is a (normalized, redaction-safe) providerId, and providerId itself is surfaced
// verbatim, so keep supersededBy verbatim too — pseudonymizing only it would break correlation
// with the winning provider in the same snapshot (and diverges from the audio-input pattern).
return _summaryMonitoringProvider(provider) || {};
}
function _redactedMonitoringSession(session, pseudonymize) {
if (!session) return null;
const sourceRef = _plainObject(session.sourceRef);
// Keep an internally-generated `input-open-<n>` id verbatim so monitoring sessions still
// correlate with audio-input openSessions[] in the same snapshot (and match the diagnostics
// schema); pseudonymize anything else as defense-in-depth.
const _openId = value => (value && /^input-open-\d+$/.test(value)) ? value : (value ? pseudonymize(value, 'input-open') : '');
return {
monitoringId: session.monitoringId ? pseudonymize(session.monitoringId, 'monitoring') : '',
sessionKey: session.sessionKey ? pseudonymize(session.sessionKey, 'monitoring-session') : '',
providerId: session.providerId || '',
logicalMonitoringKey: session.logicalMonitoringKey || '',
sourceRef: {
logicalSourceKey: sourceRef.logicalSourceKey || '',
sourceId: sourceRef.sourceId ? pseudonymize(sourceRef.sourceId, 'source') : '',
providerId: sourceRef.providerId || '',
availability: sourceRef.availability || '',
channelShape: sourceRef.channelShape || '',
openSessionId: _openId(sourceRef.openSessionId),
},
openInputSessionId: _openId(session.openInputSessionId),
state: session.state || 'unknown',
requesters: Array.isArray(session.requesters) ? session.requesters.map(requester => ({ requesterId: requester.requesterId, purpose: requester.purpose || '', requiredChannelShape: requester.requiredChannelShape || 'unknown', directMonitorRequirement: requester.directMonitorRequirement || 'unknown', status: requester.status || '', reason: requester.reason || '' })) : [],
directMonitor: _clone(session.directMonitor),
latencySummary: _clone(session.latencySummary),
reason: _boundedReason(session.reason),
startedAt: session.startedAt || '',
lastUsedAt: session.lastUsedAt || '',
stoppedAt: session.stoppedAt || '',
updatedAt: session.updatedAt || '',
};
}
function _redactedRoute(route, pseudonymize) {
return {
...route,
devicePseudonym: route.devicePseudonym ? pseudonymize(route.devicePseudonym, 'device') : '',
};
}
function _domainBridges(domain) {
return Array.from(currentSession.bridges.values()).filter(bridge => bridge.domain === domain).map(_clone);
}
function snapshot() {
// One per-bundle pseudonymizer for the whole snapshot: sources correlate
// within this export but pseudonyms are not stable across exports.
const pseudonymize = _newPseudonymizer();
_refreshInputDuplicateSuppression();
_refreshMonitoringDuplicateSuppression();
const inputSources = Array.from(currentSession.inputSources.values()).slice(-MAX_DOMAIN_ITEMS);
const openInputSessions = Array.from(currentSession.openInputSessions.values()).slice(-MAX_DOMAIN_ITEMS);
const monitoringProviders = Array.from(currentSession.monitoringProviders.values()).slice(-MAX_DOMAIN_ITEMS);
const monitoringSessions = Array.from(currentSession.monitoringSessions.values()).slice(-MAX_DOMAIN_ITEMS);
return {
schema: SCHEMA,
session: {
sessionId: _redactString(currentSession.sessionId),
playerId: currentSession.playerId,
songKey: currentSession.songKey ? pseudonymize(currentSession.songKey, 'song') : '',
songFormat: currentSession.songFormat,
state: currentSession.state,
route: _redactedRoute(currentSession.routeState, pseudonymize),
analyser: _clone(currentSession.analyserState),
createdAt: currentSession.createdAt,
updatedAt: currentSession.updatedAt,
},
domains: {
'audio-mix': {
state: currentSession.state,
participants: Array.from(currentSession.mixParticipants.values()).map(_summaryParticipant),
faders: _visibleFaderParticipants().map(participant => _summaryFader(participant)),
requiredKinds: Object.fromEntries(REQUIRED_MIX_KINDS.map(kind => [kind, Array.from(currentSession.mixParticipants.values()).some(participant => participant.kind === kind)])),
route: _redactedRoute(currentSession.routeState, pseudonymize),
analyser: _clone(currentSession.analyserState),
bridges: _domainBridges('audio-mix'),
},
'audio-input': {
sources: inputSources.map(source => _redactedSource(source, pseudonymize)),
selected: _redactedSelectedInput(_syncSelectedInput(), pseudonymize),
openSessions: openInputSessions.map(session => _redactedOpenSession(session, pseudonymize)),
totalSources: currentSession.inputSources.size,
totalOpenSessions: currentSession.openInputSessions.size,
storageStatus: currentSession.storageStatus || _storageStatus(),
bridges: _domainBridges('audio-input'),
},
'audio-monitoring': {
providers: monitoringProviders.map(provider => _redactedMonitoringProvider(provider)),
selectedProvider: _summarySelectedMonitoringProvider(_syncSelectedMonitoringProvider()),
sessions: monitoringSessions.map(session => _redactedMonitoringSession(session, pseudonymize)),
totalProviders: currentSession.monitoringProviders.size,
totalSessions: currentSession.monitoringSessions.size,
directMonitor: _clone(currentSession.directMonitor),
bridges: _domainBridges('audio-monitoring'),
},
stems: {
owner: _clone(currentSession.stemOwner),
claims: Array.from(currentSession.stemClaims.values()).map(_clone),
bridges: _domainBridges('stems'),
},
},
recentOutcomes: currentSession.outcomes.map(outcome => _redactedOutcome(outcome, pseudonymize)),
};
}
function _payload(ctx) {
return _plainObject(ctx && ctx.payload);
}
function _target(ctx) {
const target = _plainObject(ctx && ctx.target);
return { ..._payload(ctx), ...target };
}
function _audioMixCommand(commandName, ctx = {}) {
const payload = _target(ctx);
if (commandName === 'inspect') return _handled(snapshot().domains['audio-mix']);
if (commandName === 'list-faders') return listFaders();
if (commandName === 'get-fader-value') return getFaderValue(payload);
if (commandName === 'set-fader-value') return setFaderValue(payload);
if (commandName === 'inspect-route') return _handled(_clone(snapshot().domains['audio-mix'].route));
if (commandName === 'inspect-analyser') return _handled(_clone(snapshot().domains['audio-mix'].analyser));
if (commandName === 'register-participant') return registerMixParticipant(payload);
if (commandName === 'unregister-participant') return unregisterMixParticipant(payload.participantId || payload.id);
return _degraded(`Unsupported audio-mix command: ${commandName}`);
}
function _audioInputCommand(commandName, ctx = {}) {
const payload = _target(ctx);
if (commandName === 'inspect') return inspectInput(payload);
if (commandName === 'list-sources') return listInputSources(payload);
if (commandName === 'register-source') return registerInputSource(payload);
if (commandName === 'unregister-source') return unregisterInputSource(payload);
if (commandName === 'select-source') return selectInputSource(payload, ctx.requester);
if (commandName === 'open-source') return openInputSource(payload, ctx.requester);
if (commandName === 'close-source') return closeInputSource(payload, ctx.requester);
return _degraded(`Unsupported audio-input command: ${commandName}`);
}
function _audioMonitoringCommand(commandName, ctx = {}) {
const payload = _target(ctx);
if (commandName === 'inspect') return inspectMonitoring(payload);
if (commandName === 'list-providers') return listMonitoringProviders(payload);
if (commandName === 'register-provider') return registerMonitoringProvider(payload);
if (commandName === 'unregister-provider') return unregisterMonitoringProvider(payload);
if (commandName === 'select-provider') return selectMonitoringProvider(payload, ctx.requester);
if (commandName === 'start') return startMonitoring(payload, ctx.requester);
if (commandName === 'stop') return stopMonitoring(payload, ctx.requester);
if (commandName === 'set-direct-monitor') return setDirectMonitoringState(payload, ctx.requester);
return _degraded(`Unsupported audio-monitoring command: ${commandName}`);
}
function _stemsCommand(commandName, ctx = {}) {
const payload = _target(ctx);
if (commandName === 'inspect') {
const stems = snapshot().domains.stems;
return stems.owner ? _handled(stems) : _noOwner('No active stem owner is available', stems);
}
if (commandName === 'mute') return muteStems({ ...payload, requester: ctx.requester });
if (commandName === 'restore') return restoreStems({ ...payload, requester: ctx.requester });
return _degraded(`Unsupported stems command: ${commandName}`);
}
function _registerDomains() {
capabilities.registerOwner('audio-mix', {
pluginId: OWNER_ID,
kind: 'provider-coordinator',
commands: ['inspect', 'list-faders', 'get-fader-value', 'set-fader-value', 'inspect-route', 'inspect-analyser', 'register-participant', 'unregister-participant'],
operations: ['fader.get-value', 'fader.set-value', 'analyser.get-summary', 'route.get-current'],
events: ['participant-registered', 'participant-removed', 'fader-value-changed', 'fader-unavailable', 'route-changed', 'route-degraded', 'analyser-changed', 'analyser-unavailable', 'bridge-hit'],
safety: 'safe',
description: 'Coordinates the active player audio mix, route, fader participants, analyser inspection, and audio compatibility bridge usage.',
handlers: {
inspect: ctx => _audioMixCommand('inspect', ctx),
'list-faders': ctx => _audioMixCommand('list-faders', ctx),
'get-fader-value': ctx => _audioMixCommand('get-fader-value', ctx),
'set-fader-value': ctx => _audioMixCommand('set-fader-value', ctx),
'inspect-route': ctx => _audioMixCommand('inspect-route', ctx),
'inspect-analyser': ctx => _audioMixCommand('inspect-analyser', ctx),
'register-participant': ctx => _audioMixCommand('register-participant', ctx),
'unregister-participant': ctx => _audioMixCommand('unregister-participant', ctx),
},
});
capabilities.registerOwner('audio-input', {
pluginId: OWNER_ID,
kind: 'provider-coordinator',
commands: ['inspect', 'list-sources', 'register-source', 'unregister-source', 'select-source', 'open-source', 'close-source'],
operations: ['source.enumerate', 'source.describe', 'source.open', 'source.close'],
events: ['source-registered', 'source-removed', 'source-selected', 'source-availability-changed', 'permission-denied', 'source-opened', 'source-open-degraded', 'source-closed', 'bridge-hit'],
safety: 'sensitive',
description: 'Coordinates redaction-safe input source identity, availability, selection, open-session state, and bridge diagnostics.',
handlers: {
inspect: ctx => _audioInputCommand('inspect', ctx),
'list-sources': ctx => _audioInputCommand('list-sources', ctx),
'register-source': ctx => _audioInputCommand('register-source', ctx),
'unregister-source': ctx => _audioInputCommand('unregister-source', ctx),
'select-source': ctx => _audioInputCommand('select-source', ctx),
'open-source': ctx => _audioInputCommand('open-source', ctx),
'close-source': ctx => _audioInputCommand('close-source', ctx),
},
});
capabilities.registerOwner('audio-monitoring', {
pluginId: OWNER_ID,
kind: 'provider-coordinator',
commands: ['inspect', 'list-providers', 'register-provider', 'unregister-provider', 'select-provider', 'start', 'stop', 'set-direct-monitor'],
operations: ['monitoring.start', 'monitoring.stop', 'monitoring.status', 'monitoring.set-direct-monitor'],
events: ['provider-registered', 'provider-removed', 'provider-availability-changed', 'provider-selected', 'provider-selection-required', 'monitoring-start-requested', 'monitoring-started', 'monitoring-degraded', 'monitoring-unavailable', 'monitoring-failed', 'monitoring-denied', 'monitoring-user-action-required', 'monitoring-stopped', 'monitoring-orphaned', 'direct-monitor-changed', 'permission-denied', 'bridge-hit'],
safety: 'sensitive',
description: 'Coordinates monitoring provider selection, live monitoring lifecycle, direct-monitor state, consent/availability state, and monitoring compatibility bridge diagnostics.',
handlers: {
inspect: ctx => _audioMonitoringCommand('inspect', ctx),
'list-providers': ctx => _audioMonitoringCommand('list-providers', ctx),
'register-provider': ctx => _audioMonitoringCommand('register-provider', ctx),
'unregister-provider': ctx => _audioMonitoringCommand('unregister-provider', ctx),
'select-provider': ctx => _audioMonitoringCommand('select-provider', ctx),
start: ctx => _audioMonitoringCommand('start', ctx),
stop: ctx => _audioMonitoringCommand('stop', ctx),
'set-direct-monitor': ctx => _audioMonitoringCommand('set-direct-monitor', ctx),
},
});
capabilities.registerParticipant(OWNER_ID, {
stems: {
roles: ['coordinator'],
kind: 'provider-coordinator',
commands: ['inspect', 'mute', 'restore'],
operations: ['stem.get-state', 'stem.apply-automation', 'stem.restore-automation'],
events: ['owner-available', 'owner-unavailable', 'automation-applied', 'automation-restored', 'automation-overridden', 'claim-orphaned', 'bridge-hit'],
safety: 'safe',
compatibility: 'none',
ownership: 'multi-provider',
runtime: true,
version: 1,
description: 'Coordinates the active Stems provider and temporary stem automation claims without taking ownership of stem playback.',
handlers: {
inspect: ctx => _stemsCommand('inspect', ctx),
mute: ctx => _stemsCommand('mute', ctx),
restore: ctx => _stemsCommand('restore', ctx),
},
},
});
}
function _registerBridgeMetadata() {
if (typeof capabilities.registerCompatibilityShim !== 'function') return;
const shims = [
{ shimId: 'audio-mix.fader-registry', capability: 'audio-mix', source: OWNER_ID, legacySurface: 'window.feedBack.audio.registerFader', reason: 'Legacy mixer faders are attributed as audio-mix participants.' },
{ shimId: 'audio-mix.song-volume', capability: 'audio-mix', source: OWNER_ID, legacySurface: 'applySongVolume', reason: 'Legacy song volume writes are attributed as audio-mix route/fader bridge hits.' },
{ shimId: 'audio-mix.analyser', capability: 'audio-mix', source: OWNER_ID, legacySurface: '3D Highway analyser fallback', reason: 'Legacy analyser taps are attributed until renderers request analyser providers through audio-mix.' },
{ shimId: 'audio-input.legacy-source', capability: 'audio-input', source: OWNER_ID, legacySurface: 'plugin/browser input source handoff', reason: 'Legacy input source handoffs are attributed while note-detection input providers migrate to audio-input.' },
{ shimId: 'audio-monitoring.audio-barrier', capability: 'audio-monitoring', source: OWNER_ID, legacySurface: 'window.feedBackAudioBarrier', reason: 'Legacy audio startup barriers are attributed as monitoring readiness bridges.' },
{ shimId: 'stems.master-volume', capability: 'stems', source: OWNER_ID, legacySurface: 'window.feedBack.stems.setMasterVolume', reason: 'Legacy Stems master volume calls are attributed while native owner operations are staged.' },
{ shimId: 'stems.private-state', capability: 'stems', source: OWNER_ID, legacySurface: 'plugin-specific Stems/NAM ducking handshake', reason: 'Existing stem automation requesters are tracked until they migrate to native claims.' },
];
for (const shim of shims) capabilities.registerCompatibilityShim({ ...shim, status: 'active' });
}
function _contributeDiagnostics() {
const diagnostics = window.feedBack && window.feedBack.diagnostics;
if (diagnostics && typeof diagnostics.contribute === 'function') {
try { diagnostics.contribute('audio-session', snapshot()); }
catch (_) { /* diagnostics must not break playback */ }
}
}
const api = {
version: 1,
startSession,
stopSession,
setRoute,
setAnalyser,
registerMixParticipant,
unregisterMixParticipant,
listFaders,
getFaderValue,
setFaderValue,
registerInputSource,
unregisterInputSource,
selectInputSource,
listInputSources,
inspectInput,
enumerateInputSources,
openInputSource,
closeInputSource,
registerMonitoringProvider,
unregisterMonitoringProvider,
selectMonitoringProvider,
listMonitoringProviders,
inspectMonitoring,
startMonitoring,
stopMonitoring,
setDirectMonitoringState,
registerStemOwner,
muteStems,
restoreStems,
recordStemManualOverride,
recordBridgeHit,
recordOutcome: (entry) => { const outcome = _recordOutcome(entry); _touch(); return _clone(outcome); },
snapshot,
getDiagnostics: snapshot,
};
window.feedBack.audioSession = api;
_registerDomains();
_registerBridgeMetadata();
_contributeDiagnostics();
})();