@agimon-ai/po-voice 0.0.0-alpha.113 → 0.0.0-alpha.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,7 @@ node_fs = require_voiceWorkerProtocol.__toESM(node_fs, 1);
13
13
  let node_path = require("node:path");
14
14
  node_path = require_voiceWorkerProtocol.__toESM(node_path, 1);
15
15
  let node_crypto = require("node:crypto");
16
+ let _agimon_ai_po_core_narration = require("@agimon-ai/po-core/narration");
16
17
  let _agimon_ai_po_core_telemetry = require("@agimon-ai/po-core/telemetry");
17
18
  let _agimon_ai_po_core_voiceTools = require("@agimon-ai/po-core/voiceTools");
18
19
  let _agimon_ai_po_core_piSettings = require("@agimon-ai/po-core/piSettings");
@@ -20,12 +21,14 @@ let typebox_value = require("typebox/value");
20
21
  let _agimon_ai_po_core_serverPlugin = require("@agimon-ai/po-core/serverPlugin");
21
22
  let _agimon_ai_po_core_extensionFile = require("@agimon-ai/po-core/extensionFile");
22
23
  let _agimon_ai_po_session = require("@agimon-ai/po-session");
24
+ let _agimon_ai_po_core_approval = require("@agimon-ai/po-core/approval");
23
25
  let _agimon_ai_po_core_clientMetadata = require("@agimon-ai/po-core/clientMetadata");
26
+ let _agimon_ai_po_core_hubChannel = require("@agimon-ai/po-core/hubChannel");
27
+ let _agimon_ai_po_core_notification = require("@agimon-ai/po-core/notification");
28
+ let _agimon_ai_po_notification = require("@agimon-ai/po-notification");
24
29
  let xstate = require("xstate");
25
30
  let _agimon_ai_po_config = require("@agimon-ai/po-config");
26
31
  let _agimon_ai_po_core_chatgptAuth = require("@agimon-ai/po-core/chatgptAuth");
27
- let _agimon_ai_po_core_approval = require("@agimon-ai/po-core/approval");
28
- let _agimon_ai_po_core_hubChannel = require("@agimon-ai/po-core/hubChannel");
29
32
  let _agimon_ai_po_core_packageApi = require("@agimon-ai/po-core/packageApi");
30
33
  let _agimon_ai_po_minor_mode = require("@agimon-ai/po-minor-mode");
31
34
  let _agimon_ai_po_core_pluginProtocol = require("@agimon-ai/po-core/pluginProtocol");
@@ -3220,7 +3223,6 @@ var GlobalLiveCompanion = class {
3220
3223
  spokenRequest = "";
3221
3224
  approvalChallenges = /* @__PURE__ */ new Map();
3222
3225
  attentionSubscriptions = /* @__PURE__ */ new Map();
3223
- reminders = /* @__PURE__ */ new Map();
3224
3226
  feedbackId;
3225
3227
  feedbackRevision = 0;
3226
3228
  clock = new require_voiceWorkerProtocol.SystemClock();
@@ -3269,7 +3271,6 @@ var GlobalLiveCompanion = class {
3269
3271
  if (state === "disabled") {
3270
3272
  this.lifecycle = {};
3271
3273
  this.pendingActivation = void 0;
3272
- this.clearReminders();
3273
3274
  this.feedbackId = void 0;
3274
3275
  this.approvalChallenges.clear();
3275
3276
  this.approvalTurn = -1;
@@ -3494,7 +3495,6 @@ var GlobalLiveCompanion = class {
3494
3495
  }
3495
3496
  await this.live.announce("selected-feedback");
3496
3497
  await this.live.announce(`attention:${targetSessionId}`);
3497
- this.cancelReminder(targetSessionId);
3498
3498
  await this.readFeedback(target);
3499
3499
  for (const item of transfer.held) {
3500
3500
  if (this.transfer !== transfer || this.stopped) return false;
@@ -3527,7 +3527,6 @@ var GlobalLiveCompanion = class {
3527
3527
  this.pendingActivation = void 0;
3528
3528
  if (this.transfer?.held.length) this.options.onNotice(`Explicit Voice stop discarded ${this.transfer.held.length} held requests for ${this.transfer.targetSessionId}.`);
3529
3529
  const previousRoute = this.route;
3530
- this.clearReminders();
3531
3530
  this.feedbackId = void 0;
3532
3531
  this.approvalChallenges.clear();
3533
3532
  this.approvalTurn = -1;
@@ -3574,7 +3573,6 @@ var GlobalLiveCompanion = class {
3574
3573
  if (this.selectedSessionId === sessionId) this.selectedSessionId = void 0;
3575
3574
  this.approvals.delete(sessionId);
3576
3575
  this.approvalChallenges.delete(sessionId);
3577
- this.cancelReminder(sessionId);
3578
3576
  if (this.route?.scope.sessionId === sessionId) this.stop().catch((error) => this.options.onNotice(`Voice route takeover failed: ${errorMessage(error)}`));
3579
3577
  }
3580
3578
  /** Tracks the person's own turns, never model-supplied text, for single-use approval challenges. */
@@ -3804,16 +3802,18 @@ var GlobalLiveCompanion = class {
3804
3802
  this.fences.delete(sessionId);
3805
3803
  this.approvals.delete(sessionId);
3806
3804
  this.approvalChallenges.delete(sessionId);
3807
- this.cancelReminder(sessionId);
3808
3805
  this.announce(`attention:${sessionId}`);
3809
3806
  this.announce(`approval:${sessionId}`);
3810
3807
  }
3808
+ get announcementActivation() {
3809
+ return this.live.state === "active" ? this.activationId : void 0;
3810
+ }
3811
3811
  announce(key, text) {
3812
3812
  this.live.announce(key, text).catch((error) => this.options.onNotice(`Voice announcement failed: ${errorMessage(error)}`));
3813
3813
  }
3814
3814
  /** A pending approval came or went: offer a spoken answer for `ask`, point to the app for `human`. */
3815
3815
  updateApproval(scope, approval, previous) {
3816
- if (this.options.reserveSession && scope.sessionId !== this.selectedSessionId && scope.sessionId !== this.route?.scope.sessionId) return;
3816
+ if (scope.sessionId !== this.selectedSessionId && scope.sessionId !== this.route?.scope.sessionId) return;
3817
3817
  if (approval?.id === previous?.id) return;
3818
3818
  this.approvalChallenges.delete(scope.sessionId);
3819
3819
  this.announce(`approval:${scope.sessionId}`);
@@ -3838,11 +3838,8 @@ var GlobalLiveCompanion = class {
3838
3838
  this.updateApproval(scope, approval);
3839
3839
  }
3840
3840
  updateAttention(scope, next, previous) {
3841
- if (this.options.reserveSession && scope.sessionId !== this.selectedSessionId && scope.sessionId !== this.route?.scope.sessionId) return;
3842
- if (next.dialog?.id !== previous?.dialog?.id) {
3843
- this.cancelReminder(scope.sessionId);
3844
- this.announce(`attention:${scope.sessionId}`);
3845
- }
3841
+ if (scope.sessionId !== this.selectedSessionId && scope.sessionId !== this.route?.scope.sessionId) return;
3842
+ if (next.dialog?.id !== previous?.dialog?.id) this.announce(`attention:${scope.sessionId}`);
3846
3843
  if (this.live.state !== "active") return;
3847
3844
  if (this.route?.scope.sessionId === scope.sessionId) {
3848
3845
  if (next.dialog?.id !== previous?.dialog?.id) {
@@ -3853,27 +3850,6 @@ var GlobalLiveCompanion = class {
3853
3850
  if (next.dialog?.purpose === "feedback" && next.dialog.id !== previous?.dialog?.id) this.readFeedback(this.route).catch((error) => this.options.onNotice(errorMessage(error)));
3854
3851
  return;
3855
3852
  }
3856
- const name = next.name || scope.sessionId;
3857
- const dialog = next.dialog;
3858
- if (dialog && dialog.id !== previous?.dialog?.id) {
3859
- const text = dialog.purpose === "feedback" ? `${name} asks: ${dialog.title.slice(0, 200)}. Switch there?` : `${name} is waiting for approval in the app.`;
3860
- this.announce(`attention:${scope.sessionId}`, text);
3861
- if (dialog.purpose === "feedback") {
3862
- const timer = setTimeout(() => {
3863
- this.reminders.delete(scope.sessionId);
3864
- if (this.attention.get(scope.sessionId)?.dialog?.id === dialog.id && this.route?.scope.sessionId !== scope.sessionId && this.live.state === "active") this.announce(`attention:${scope.sessionId}`, `Reminder: ${text}`);
3865
- }, 3e5);
3866
- this.reminders.set(scope.sessionId, timer);
3867
- }
3868
- } else if (previous?.phase === "turn" && next.phase === "idle" && !dialog) {
3869
- const activation = this.activationId;
3870
- const fence = this.fences.get(scope.sessionId);
3871
- this.agentRequest({ scope }, "/live/agent/outcome", "GET").then((outcome) => {
3872
- if (this.attention.get(scope.sessionId) !== next || this.activationId !== activation || this.fences.get(scope.sessionId) !== fence || this.route?.scope.sessionId === scope.sessionId || this.live.state !== "active") return;
3873
- const text = object(outcome)?.text;
3874
- this.announce(`attention:${scope.sessionId}`, `${name} finished: ${typeof text === "string" && text.trim() ? text : "No final text outcome was reported."}`);
3875
- }).catch((error) => this.options.onNotice(`Voice outcome unavailable: ${errorMessage(error)}`));
3876
- }
3877
3853
  }
3878
3854
  resumeAttention() {
3879
3855
  for (const scope of this.hub?.sessions() ?? []) {
@@ -3881,19 +3857,10 @@ var GlobalLiveCompanion = class {
3881
3857
  if (next?.dialog) this.updateAttention(scope, next);
3882
3858
  }
3883
3859
  }
3884
- cancelReminder(sessionId) {
3885
- const timer = this.reminders.get(sessionId);
3886
- if (timer !== void 0) clearTimeout(timer);
3887
- this.reminders.delete(sessionId);
3888
- }
3889
- clearReminders() {
3890
- for (const id of this.reminders.keys()) this.cancelReminder(id);
3891
- }
3892
3860
  clearAttention() {
3893
3861
  for (const stop of this.attentionSubscriptions.values()) stop();
3894
3862
  this.attentionSubscriptions.clear();
3895
3863
  this.attention.clear();
3896
- this.clearReminders();
3897
3864
  }
3898
3865
  async readFeedback(route) {
3899
3866
  const revision = ++this.feedbackRevision;
@@ -3906,7 +3873,6 @@ var GlobalLiveCompanion = class {
3906
3873
  }
3907
3874
  if (this.feedbackId === feedback.id) return;
3908
3875
  this.feedbackId = feedback.id;
3909
- this.cancelReminder(route.scope.sessionId);
3910
3876
  await this.live.announce(`attention:${route.scope.sessionId}`);
3911
3877
  const question = JSON.stringify(feedback);
3912
3878
  if (Buffer.byteLength(question, "utf8") > 12288) {
@@ -4121,6 +4087,7 @@ var GlobalLiveManager = class GlobalLiveManager {
4121
4087
  host;
4122
4088
  closed = false;
4123
4089
  cleanupTimer;
4090
+ notices = /* @__PURE__ */ new Map();
4124
4091
  scheduleCleanup() {
4125
4092
  if (this.closed || this.cleanupTimer !== void 0 || this.channels.size === 0) return;
4126
4093
  this.cleanupTimer = setTimeout(() => {
@@ -4146,6 +4113,209 @@ var GlobalLiveManager = class GlobalLiveManager {
4146
4113
  static create(options) {
4147
4114
  return Promise.resolve(new GlobalLiveManager(options));
4148
4115
  }
4116
+ noticeTarget(deviceId, source) {
4117
+ if (this.closed || source.scope.parentSessionId !== void 0 || source.scope.sessionContext?.parentSessionId !== void 0 || source.attention?.parentSessionId !== void 0 || source.scope.sessionId.startsWith("peer/")) return;
4118
+ const sourceOwner = this.options.sessionOwners.get(source.scope.sessionId);
4119
+ if (sourceOwner && sourceOwner.deviceId !== deviceId) return;
4120
+ const companion = this.channels.get(deviceId)?.companion;
4121
+ const status = companion?.status;
4122
+ const activationId = companion?.announcementActivation;
4123
+ const coordinator = this.coordinators.get(deviceId);
4124
+ const sessionId = activationId ? status?.activeSessionId : !status || status.state === "disabled" ? coordinator?.activeSessionId() : void 0;
4125
+ if (!sessionId || sessionId === source.scope.sessionId || sessionId.startsWith("peer/")) return;
4126
+ const owner = this.options.sessionOwners.get(sessionId);
4127
+ if (owner?.deviceId !== deviceId) return;
4128
+ const endpoint = this.agents.get(sessionId)?.endpoint;
4129
+ const registration = coordinator?.registration(sessionId);
4130
+ if (!activationId && (!endpoint?.narrate || !registration?.active || registration.deviceId !== deviceId)) return;
4131
+ return {
4132
+ deviceId,
4133
+ sessionId,
4134
+ generation: owner.generation,
4135
+ activationId,
4136
+ ...activationId ? {} : {
4137
+ endpoint,
4138
+ leaseId: registration?.leaseId
4139
+ }
4140
+ };
4141
+ }
4142
+ noticeTargets(source) {
4143
+ return [.../* @__PURE__ */ new Set([...this.channels.keys(), ...this.coordinators.keys()])].flatMap((deviceId) => {
4144
+ const target = this.noticeTarget(deviceId, source);
4145
+ return target ? [target] : [];
4146
+ });
4147
+ }
4148
+ currentTarget(source, target) {
4149
+ if (this.notices.get(source.scope.sessionId) !== source) return false;
4150
+ const current = this.noticeTarget(target.deviceId, source);
4151
+ return current?.sessionId === target.sessionId && current.generation === target.generation && current.activationId === target.activationId && current.endpoint === target.endpoint && current.leaseId === target.leaseId;
4152
+ }
4153
+ clearNotice(source) {
4154
+ for (const { target, controller } of source.pending.values()) {
4155
+ controller.abort();
4156
+ if (target.activationId === this.channels.get(target.deviceId)?.companion.announcementActivation) this.channels.get(target.deviceId)?.companion.announce(`notice:${source.scope.sessionId}`);
4157
+ }
4158
+ source.pending.clear();
4159
+ }
4160
+ pruneNotices() {
4161
+ for (const source of this.notices.values()) for (const [deviceId, pending] of source.pending) {
4162
+ if (this.currentTarget(source, pending.target)) continue;
4163
+ pending.controller.abort();
4164
+ this.channels.get(deviceId)?.companion.announce(`notice:${source.scope.sessionId}`);
4165
+ source.pending.delete(deviceId);
4166
+ }
4167
+ }
4168
+ sendNotice(source, text, targets = this.noticeTargets(source)) {
4169
+ this.clearNotice(source);
4170
+ const request = (0, _agimon_ai_po_core_narration.createNarrationRequest)(text.replace(/([.!?])\.$/u, "$1"));
4171
+ if (!request) return;
4172
+ for (const target of targets) {
4173
+ if (!this.currentTarget(source, target)) continue;
4174
+ const controller = new AbortController();
4175
+ const pending = {
4176
+ target,
4177
+ controller
4178
+ };
4179
+ source.pending.set(target.deviceId, pending);
4180
+ Promise.resolve().then(async () => {
4181
+ if (controller.signal.aborted || !this.currentTarget(source, target)) return;
4182
+ if (target.activationId) {
4183
+ this.channels.get(target.deviceId)?.companion.announce(`notice:${source.scope.sessionId}`, request.text);
4184
+ return;
4185
+ }
4186
+ const timer = setInterval(() => {
4187
+ if (!this.currentTarget(source, target)) controller.abort();
4188
+ }, 250);
4189
+ timer.unref?.();
4190
+ const stopChecking = () => clearInterval(timer);
4191
+ controller.signal.addEventListener("abort", stopChecking, { once: true });
4192
+ try {
4193
+ await target.endpoint?.narrate?.(request.text, target.deviceId, controller.signal);
4194
+ } finally {
4195
+ clearInterval(timer);
4196
+ controller.signal.removeEventListener("abort", stopChecking);
4197
+ if (source.pending.get(target.deviceId) === pending) source.pending.delete(target.deviceId);
4198
+ }
4199
+ }).catch((error) => {
4200
+ if (!controller.signal.aborted) this.options.companion.onNotice(`Voice notice failed: ${String(error)}`);
4201
+ });
4202
+ }
4203
+ }
4204
+ inputNotice(source) {
4205
+ const dialog = source.attention?.dialog;
4206
+ const approval = source.approval ?? dialog?.approval;
4207
+ const inputId = approval?.id ?? dialog?.id;
4208
+ if (source.inputId === inputId) return;
4209
+ source.inputId = inputId;
4210
+ this.clearNotice(source);
4211
+ if (!inputId) return;
4212
+ if (source.timer !== void 0) clearTimeout(source.timer);
4213
+ source.timer = void 0;
4214
+ const title = approval ? "approval in the app" : dialog.title.slice(0, 200);
4215
+ this.sendNotice(source, `${(source.attention?.name || source.scope.name || source.scope.sessionId).slice(0, 200)} needs your input: ${title}.`);
4216
+ }
4217
+ finishLater(source, targets, delayMs) {
4218
+ const run = source.run;
4219
+ source.timer = setTimeout(() => {
4220
+ source.timer = void 0;
4221
+ const current = () => this.notices.get(source.scope.sessionId) === source && source.run === run && source.attention?.phase === "idle" && !source.inputId;
4222
+ (async () => {
4223
+ if (!current() || !targets.some((target) => this.currentTarget(source, target))) return;
4224
+ try {
4225
+ const state = await this.host?.sessionService?.readExecutionState?.(source.scope.sessionId);
4226
+ if (!current()) return;
4227
+ const decision = (0, _agimon_ai_po_notification.decideSessionFinished)(state, source.holdingSince, Date.now());
4228
+ if (decision.action === "finished") this.sendNotice(source, `${(source.attention?.name || source.scope.name || source.scope.sessionId).slice(0, 200)} finished.`, targets);
4229
+ else if (decision.action === "hold" || decision.action === "recheck") {
4230
+ if (decision.action === "hold") source.holdingSince = decision.holdingSince;
4231
+ this.finishLater(source, targets, decision.delayMs);
4232
+ } else if (decision.action === "unavailable") throw new Error("Execution state unavailable");
4233
+ } catch {
4234
+ if (current() && !source.warned) {
4235
+ source.warned = true;
4236
+ this.options.companion.onNotice("Voice completion check unavailable.");
4237
+ }
4238
+ }
4239
+ })();
4240
+ }, delayMs);
4241
+ source.timer.unref?.();
4242
+ }
4243
+ watchNotices(scope) {
4244
+ const host = this.host;
4245
+ if (!host || this.notices.has(scope.sessionId)) return;
4246
+ const source = {
4247
+ scope,
4248
+ stop() {},
4249
+ running: false,
4250
+ run: 0,
4251
+ entries: /* @__PURE__ */ new Set(),
4252
+ pending: /* @__PURE__ */ new Map()
4253
+ };
4254
+ this.notices.set(scope.sessionId, source);
4255
+ let seeding = true;
4256
+ const stopAttention = host.directEvents.subscribe(_agimon_ai_po_core_hubChannel.PO_SESSION_ATTENTION_EVENT, scope.sessionId, (payload) => {
4257
+ const next = payload;
4258
+ if (!next || ![
4259
+ "idle",
4260
+ "turn",
4261
+ "retry",
4262
+ "compaction"
4263
+ ].includes(next.phase) || typeof next.awaitingInput !== "boolean" || next.name !== void 0 && typeof next.name !== "string" || next.parentSessionId !== void 0 && typeof next.parentSessionId !== "string" || next.dialog !== void 0 && (!next.dialog || typeof next.dialog.id !== "string" || typeof next.dialog.title !== "string")) return;
4264
+ source.attention = next;
4265
+ if (seeding) {
4266
+ source.running = next.phase !== "idle";
4267
+ source.inputId = next.dialog?.approval?.id ?? next.dialog?.id;
4268
+ return;
4269
+ }
4270
+ if (next.phase !== "idle" && !source.running) {
4271
+ source.running = true;
4272
+ source.run += 1;
4273
+ source.holdingSince = void 0;
4274
+ source.entries.clear();
4275
+ if (source.timer !== void 0) clearTimeout(source.timer);
4276
+ source.timer = void 0;
4277
+ this.clearNotice(source);
4278
+ }
4279
+ this.inputNotice(source);
4280
+ if (next.phase === "idle" && source.running) {
4281
+ source.running = false;
4282
+ if (!source.inputId) this.finishLater(source, this.noticeTargets(source), _agimon_ai_po_notification.SESSION_FINISHED_COOLDOWN_MS);
4283
+ }
4284
+ }, { replayLatest: true });
4285
+ const stopApproval = host.directEvents.subscribe(_agimon_ai_po_core_approval.PO_APPROVAL_VOICE_EVENT, scope.sessionId, (payload) => {
4286
+ source.approval = (0, _agimon_ai_po_core_approval.parsePoApprovalPrompt)(payload);
4287
+ if (seeding) source.inputId = source.approval?.id ?? source.inputId;
4288
+ else this.inputNotice(source);
4289
+ }, { replayLatest: true });
4290
+ seeding = false;
4291
+ const stopEntries = host.directEvents.subscribe(_agimon_ai_po_core_hubChannel.PO_SESSION_NOTIFICATION_EVENT, scope.sessionId, (payload) => {
4292
+ const entry = payload;
4293
+ if (!entry || typeof entry.entryId !== "string" || !entry.entryId || !(0, _agimon_ai_po_core_notification.isPoNotificationEntryData)(entry.data) || !entry.data.attention || source.entries.has(entry.entryId)) return;
4294
+ source.entries.add(entry.entryId);
4295
+ if (source.entries.size > 256) source.entries.delete(source.entries.values().next().value);
4296
+ const targets = this.noticeTargets(source);
4297
+ const run = source.run;
4298
+ queueMicrotask(() => {
4299
+ if (this.notices.get(scope.sessionId) !== source || source.run !== run) return;
4300
+ const approval = source.approval ?? source.attention?.dialog?.approval;
4301
+ if (approval && entry.data.title === "Approval needed" && entry.data.body === approval.title) return;
4302
+ this.sendNotice(source, `${(source.attention?.name || scope.name || scope.sessionId).slice(0, 200)} needs your input: ${(entry.data.title || entry.data.body).slice(0, 200)}.`, targets);
4303
+ });
4304
+ });
4305
+ source.stop = () => {
4306
+ stopAttention();
4307
+ stopApproval();
4308
+ stopEntries();
4309
+ };
4310
+ }
4311
+ removeNotices(sessionId) {
4312
+ const source = this.notices.get(sessionId);
4313
+ if (!source) return;
4314
+ source.stop();
4315
+ if (source.timer !== void 0) clearTimeout(source.timer);
4316
+ this.clearNotice(source);
4317
+ this.notices.delete(sessionId);
4318
+ }
4149
4319
  reserve(deviceId, sessionId, takeoverGeneration) {
4150
4320
  const identity = this.channels.get(deviceId)?.broker.owner;
4151
4321
  if (!identity) throw new Error("Connect this device media before reserving a work session.");
@@ -4155,6 +4325,7 @@ var GlobalLiveManager = class GlobalLiveManager {
4155
4325
  if (reservations.get(sessionId) !== holder) return;
4156
4326
  reservations.delete(sessionId);
4157
4327
  this.channels.get(deviceId)?.companion.fenceWorkSession(sessionId);
4328
+ this.pruneNotices();
4158
4329
  }, takeoverGeneration);
4159
4330
  if (!result.ok) throw Object.assign(/* @__PURE__ */ new Error("This session has Voice on another device."), { owner: result.owner });
4160
4331
  reservations.set(sessionId, holder);
@@ -4229,18 +4400,23 @@ var GlobalLiveManager = class GlobalLiveManager {
4229
4400
  };
4230
4401
  }
4231
4402
  bind(host) {
4403
+ if (this.host && this.host !== host) this.unbind(this.host);
4232
4404
  this.host = host;
4405
+ for (const scope of host.sessions()) this.watchNotices(scope);
4233
4406
  for (const channel of this.channels.values()) channel.companion.bind(host);
4234
4407
  }
4235
4408
  unbind(host) {
4236
4409
  if (this.host !== host) return;
4410
+ for (const sessionId of this.notices.keys()) this.removeNotices(sessionId);
4237
4411
  for (const channel of this.channels.values()) channel.companion.unbind(host);
4238
4412
  this.host = void 0;
4239
4413
  }
4240
4414
  sessionAdded(scope) {
4415
+ this.watchNotices(scope);
4241
4416
  for (const channel of this.channels.values()) channel.companion.sessionAdded(scope);
4242
4417
  }
4243
4418
  sessionRemoved(sessionId) {
4419
+ this.removeNotices(sessionId);
4244
4420
  for (const channel of this.channels.values()) channel.companion.sessionRemoved(sessionId);
4245
4421
  for (const reservations of this.reservations.values()) {
4246
4422
  const holder = reservations.get(sessionId);
@@ -4252,8 +4428,10 @@ var GlobalLiveManager = class GlobalLiveManager {
4252
4428
  if (coordinator) this.coordinators.set(deviceId, coordinator);
4253
4429
  else this.coordinators.delete(deviceId);
4254
4430
  this.channels.get(deviceId)?.companion.setCatalog(coordinator);
4431
+ this.pruneNotices();
4255
4432
  }
4256
4433
  async refreshSelectedCatalog(deviceId) {
4434
+ this.pruneNotices();
4257
4435
  await this.channels.get(deviceId)?.companion.refreshSelectedCatalog();
4258
4436
  }
4259
4437
  liveSessionId(deviceId) {
@@ -4304,6 +4482,7 @@ var GlobalLiveManager = class GlobalLiveManager {
4304
4482
  return Response.json({ error: "Connect this device media before controlling Live Voice." }, { status: 409 });
4305
4483
  }
4306
4484
  const response = await channel.companion.api.start({}).fetch(request);
4485
+ this.pruneNotices();
4307
4486
  if (response.status !== 409) return response;
4308
4487
  const conflict = await response.clone().json().catch(() => void 0);
4309
4488
  if (conflict?.code !== "VOICE_OWNERSHIP_CONFLICT" || !conflict.owner) return response;
@@ -4320,6 +4499,7 @@ var GlobalLiveManager = class GlobalLiveManager {
4320
4499
  this.closed = true;
4321
4500
  if (this.cleanupTimer !== void 0) clearTimeout(this.cleanupTimer);
4322
4501
  this.cleanupTimer = void 0;
4502
+ for (const sessionId of this.notices.keys()) this.removeNotices(sessionId);
4323
4503
  await Promise.all([...this.channels.values()].map((channel) => channel.companion.close()));
4324
4504
  for (const agent of this.agents.values()) agent.endpoint.onResult = void 0;
4325
4505
  this.channels.clear();
@@ -4416,6 +4596,11 @@ var VoiceOwnershipCoordinator = class {
4416
4596
  this.prune();
4417
4597
  return [...this.participants.values()].find((participant) => participant.active && participant.sessionId !== exceptSessionId && !participant.sessionId.startsWith("peer/"))?.label;
4418
4598
  }
4599
+ /** An actually running local participant, never a staged activation or transfer. */
4600
+ activeSessionId() {
4601
+ this.prune();
4602
+ return [...this.participants.values()].find((participant) => participant.active && !participant.sessionId.startsWith("peer/"))?.sessionId;
4603
+ }
4419
4604
  /** Current eligible catalog, including discovered paired targets. */
4420
4605
  liveCatalog(sourceSessionId) {
4421
4606
  this.prune();
@@ -9241,7 +9426,26 @@ function createVoiceServer(host, broker, homeDirectory, hubToken, peerAgents, re
9241
9426
  context.inject([PO_VOICE_LIVE_SERVICE], (scoped) => {
9242
9427
  const companion = scoped.get(PO_VOICE_LIVE_SERVICE);
9243
9428
  if (!companion) return void 0;
9244
- return companion.attach(execution.sessionId, liveAgent);
9429
+ const lifetime = new AbortController();
9430
+ const detach = companion.attach(execution.sessionId, {
9431
+ handle: (request) => liveAgent.handle(request),
9432
+ get onResult() {
9433
+ return liveAgent.onResult;
9434
+ },
9435
+ set onResult(callback) {
9436
+ liveAgent.onResult = callback;
9437
+ },
9438
+ async narrate(text, deviceId, signal) {
9439
+ const request = (0, _agimon_ai_po_core_narration.createNarrationRequest)(text);
9440
+ const admitted = AbortSignal.any([signal, lifetime.signal]);
9441
+ if (!request || admitted.aborted || closed || mode.selectedMode !== "legacy" || mode.state !== "active" || broker.owner?.deviceId !== deviceId) return;
9442
+ await mode.narrateExternal(request.text, admitted);
9443
+ }
9444
+ });
9445
+ return () => {
9446
+ lifetime.abort();
9447
+ detach();
9448
+ };
9245
9449
  });
9246
9450
  }
9247
9451
  ],