@xema/omni-protocol 0.1.107 → 0.1.109

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.
package/guide/breaks.md CHANGED
@@ -250,291 +250,7 @@ against it (`stream.taskOffered.overCapacity`).
250
250
  idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
251
251
  it: the ceiling binds assignment, not the agent's own hand.
252
252
 
253
- ### Runtime break prerequisite checks
254
-
255
- Use `validateBreakCommand(method, request, state, context)` from the validation entry point
256
- before any agent break dispatch. The context supplies current authentication and transport;
257
- all four methods require the live login's break capability and active transport. Pass the
258
- request object only to the request method, and undefined to the other three.
259
-
260
- | Method | Required current state |
261
- | --- | --- |
262
- | `requestBreak` | `not-requested`; selected current reason code when codes exist; `canRequestBreak` or the selected reason's `alwaysAvailable` exception. Free text does not replace a code. |
263
- | `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `canRequestBreak` does not revoke the grant. |
264
- | `cancelBreak` | `awaiting-approval` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
265
- | `endBreak` | `on-break` or `starting-after-task` during reconciliation; the agent may explicitly end a requested or forced break. |
266
-
267
- Use `validateTeamCommand(request, context)` before every lead act: the team feature switch, break
268
- decisions, forcing and ending a forced break, policy, and every act on a member's call. It requires
269
- the live `lead` flag and active transport, a current team member list for any command naming a
270
- member, and the target's complete break state for forcing/ending a break. Approve/deny requires an
271
- awaiting decision; forcing a break uses the target's reason codes; ending a forced break requires a
272
- forced committed break; join and decline need the member's request on the list; coach, join-call
273
- and leave need the lead already on that member's call; an assignment named must be one the list
274
- shows the member holding. The context's target state must belong to the named member; that
275
- association and backend authorization are provider responsibilities.
276
-
277
- Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
278
- as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
279
- checks. Pending and completing tasks still count as work. Only a call a lead took over
280
- (`takenOver`) sits beside a break in effect. Validate full task shapes and the envelope separately.
281
-
282
- These validators report violations without dispatching, rewriting state or proving freshness.
283
- Recheck prerequisites atomically at the provider; client validation cannot prevent a race.
284
- Continue to use `validateResult` for each method's response vocabulary. A request acknowledgment
285
- is not a grant, and no method response overwrites a newer state emission. Rejected promises
286
- remain unknown and recover by snapshot, not automatic retry. The agent application's frozen provider set,
287
- durable mutually exclusive commit/cancel decision and reconciliation obligations under
288
- **Coordinating a multi-provider break** remain
289
- required; per-provider validation does not implement the multi-provider coordinator.
290
-
291
- ### `requestBreak(request)`
292
-
293
- **Explanatory text is optional.** An agent may omit `BreakRequest.reason` for any break type.
294
- A lead may likewise omit reason text from `force-break`. Do not require a text explanation
295
- just because a break is agent-requested or forced. If supplied, text must be nonempty; omit
296
- it rather than sending an empty string. A selected `reasonId` identifies a published break
297
- choice and is separate from this optional text.
298
-
299
- With published choices, `{ reasonId: "lunch" }` is a valid request without explanation;
300
- without published choices, `{}` is valid. A lead can similarly send a `force-break` command
301
- with its member ID and, where required, the selected reason ID, without reason text.
302
-
303
- Requests permission to stop the agent later; it does not itself stop work. The provider continues
304
- offering work and reports `awaiting-approval` or `granted` through `break-state` events. If the request is denied, the
305
- provider reports `not-requested` directly, with `decisionReason` when one was supplied.
306
-
307
- **An agent asks from anywhere — idle or on a task** — and Omni offers the request in the task
308
- workspace as it does on the idle dashboard. Asked on a task, the break is decided and committed like
309
- any other and begins when the work ends: that is `starting-after-task`, and
310
- `assertBreakBeginsAfterTask` is the scenario that holds a provider to it.
311
-
312
- #### Break reasons
313
-
314
- A provider that defines not-ready reason codes publishes them on `Snapshot.break.reasons`,
315
- and Omni returns the agent's choice as `BreakRequest.reasonId`:
316
-
317
- ```ts
318
- break: {
319
- reasons: [
320
- { id: "lunch", label: "Lunch" },
321
- { id: "training", label: "Training", group: "Scheduled" },
322
- ]
323
- }
324
- ```
325
-
326
- Reason ids are non-empty and unique within the provider. They live on the snapshot rather than the
327
- manifest because a provider may change the codes it offers during a shift; a `snapshot` event
328
- replaces the list. `BreakRequest.reason` remains available for free text and is never a substitute
329
- for `reasonId` when the provider publishes codes. A provider that defines no codes omits the
330
- field.
331
-
332
- #### One break across several providers
333
-
334
- An agent connected to several providers takes **one** break, not one per platform. Omni gathers
335
- what every connected provider offers, shows the distinct breaks, and on a choice sends one request
336
- per provider: the same `reason`, and each provider's own `reasonId`.
337
-
338
- To do that, Omni has to know when two providers mean the same break. Say so with `kind`:
339
-
340
- ```ts
341
- break: {
342
- reasons: [
343
- { id: "lunch", label: "Lunch", kind: "meal" },
344
- { id: "rest", label: "Mandatory rest period", kind: "rest", alwaysAvailable: true },
345
- ]
346
- }
347
- ```
348
-
349
- A provider decides which breaks it offers and what it calls them. `BREAK_KINDS` is the list Omni
350
- supports mapping them onto, and every member means something specific:
351
-
352
- | Kind | What it means |
353
- | --- | --- |
354
- | `short-break` | A brief rest between contacts — the comfort break a shift plan allows for. |
355
- | `meal` | A meal: lunch, dinner, whatever the shift calls it. |
356
- | `rest` | A rest period the agent is entitled to and a busy hour cannot cancel. Usually the reason also marked `alwaysAvailable`, though the two are separate: this says what the break *is*, that says whether policy can withdraw it. |
357
- | `training` | Learning something the agent is expected to know afterwards: a course, e-learning, a product walkthrough. However it is delivered, and whoever attends. |
358
- | `coaching` | Reviewing this agent's own work with somebody accountable for it: a call listened back, quality feedback, a one-to-one about their interactions. Even where the outcome is that they learn something. |
359
- | `meeting` | A scheduled gathering that is neither — a team huddle, a project call, a town hall. The agent attends and contributes; nobody is assessing their work and there is nothing they must know by the end. |
360
- | `administrative` | Paperwork and follow-up not attached to a particular contact. |
361
- | `technical` | Equipment or system trouble stopping the agent taking work: a dead headset, a phone that never registered, a tool that will not load. The one member that is not an activity — it says why the agent *cannot* work rather than what they are doing, which makes it the right home for a not-ready state raised about the agent's equipment. |
362
- | `personal` | Personal time the deployment does not classify further. |
363
- | `other` | None of the above. Matches nothing, including another provider's `other`. |
364
-
365
- **The definitions are the point, not decoration.** Ten undefined strings would be the label
366
- problem one level up: two providers could both declare `technical`, one meaning a dead headset
367
- and the other scheduled maintenance, match on it, and nothing could tell the difference.
368
-
369
- `training`, `coaching` and `meeting` can all fit one session, so take them in that order of
370
- specificity: about this agent's own work is `coaching`, else something they must know afterwards is
371
- `training`, else `meeting`.
372
-
373
- **A break somebody forced on the agent is not automatically one of these.** Where the agent was
374
- stopped rather than choosing to stop — see **Forced breaks** — set `BreakState.forced` and prefer
375
- omitting `kind` to reaching for `other`. None of the ten describes "something was done to this
376
- agent", and `other` claims a classification that was never made.
377
-
378
- Omni matches in this order:
379
-
380
- 1. **`kind`**, where both providers declare one. This wins over the label, so `{ id: "MEAL",
381
- label: "Meal", kind: "meal" }` lines up with `{ id: "lunch", label: "Lunch", kind: "meal" }`
382
- and nobody has to word their codes the same way.
383
- 2. **The label**, folded for case and surrounding spacing, where a kind is missing. `"other"`
384
- counts as missing: two providers saying `"other"` have only said their break is not on the
385
- list, which is no evidence they mean the same thing.
386
-
387
- Nothing else is matched. A break Omni cannot pair stays on its own, the agent is told how many
388
- platforms their break will actually reach, and they can pair the odd one by hand — Omni remembers
389
- that for next time.
390
-
391
- Three rules for a provider:
392
-
393
- - **Declare `kind` where you can, and leave it out where you cannot.** Leaving it out costs a
394
- little precision. Getting it wrong sends an agent to lunch on one platform and to training on
395
- another, and nothing can detect that.
396
- - **You may publish several reasons of one kind** — two meal slots, say. Omni will not choose
397
- between them for the agent; saying they are the same kind is not saying they are interchangeable.
398
- - **Do not read `reason` as your own label.** It is the agent's single choice, in whichever
399
- provider's wording they picked it from, sent unchanged to every provider in the break.
400
-
401
- A provider that publishes no reason codes still receives the request, with `reason` set and
402
- `reasonId` omitted.
403
-
404
- #### Coordinating a multi-provider break
405
-
406
- Sending the requests is a two-phase operation, not independent best effort. A provider first grants
407
- permission for the agent to stop, then Omni either commits or cancels that permission.
408
-
409
- `granted` is provider-visible state. It means only that this provider has granted the active
410
- request and is ready to stop the agent when Omni asks. It does not reveal that other providers exist
411
- or why Omni has not committed yet. While reporting `granted`, the provider remains available,
412
- continues to honour Omni's current capacity, and continues offering work normally.
413
-
414
- Omni separately tracks the aggregate agent application states `working`, `requesting-break`, `committing-break`,
415
- `cancelling-break`, and `on-break`. While `requesting-break`, Omni shows which providers remain
416
- outstanding and offers **Cancel break request**, but does not tell the agent that their break has
417
- begun.
418
-
419
- Omni offers the aggregate Break control only when every provider currently holding capacity
420
- declares `capabilities.breaks` at login. If one cannot be stopped, offering a global break would
421
- knowingly permit partial availability.
422
-
423
- Omni coordinates one break attempt as follows:
424
-
425
- 1. Freeze the providers the attempt asks: every connected provider from which the agent can
426
- currently receive work. A provider joining during the attempt is given no capacity until it
427
- finishes. A provider whose authentication is `expired` is not one the agent can receive work
428
- from and is not asked: nothing is asked of it, the break proceeds without it, and when the
429
- login is restored it is reconciled from its snapshot as a set-aside provider is, with no
430
- capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
431
- available and work continues. `assertBreakAttemptProviders` holds an agent application to this set.
432
- 2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
433
- 3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-approval` or
434
- `granted`; neither state stops work. A denial transitions directly to `not-requested` and
435
- causes Omni to take the cancel path.
436
- 4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
437
- and send `commitBreak()` to each of them. A provider then stops offering new work
438
- and reports `starting-after-task` or `on-break`. The **commit bound** — ten seconds from the
439
- decision, tunable per deployment — decides who is kept: a provider that has not applied the
440
- commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
441
- begins without it. `on-break` decides `on-break`: Omni enters it once every kept provider
442
- reports `on-break`. A kept provider reporting `starting-after-task` has applied the commit
443
- and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
444
- settled and beginning when the task ends, and offers no cancel, because the commit is durable.
445
- 5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
446
- decision timeout, or the agent cancels before commit, durably choose cancel and enter
447
- `cancelling-break`. Send `cancelBreak()` to every provider still reporting
448
- `awaiting-approval` or `granted`. Work continues during cancellation because no stop was
449
- committed. Return to `working` only after no provider retains either state.
450
-
451
- Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
452
- it never rolls that attempt back: it is reconciled by snapshot until every asked provider is stopped.
453
- A provider that reports `granted` must therefore preserve the request across reconnects and must
454
- honour a later commit or cancel. This
455
- durable promise prevents a provider from failing the commit after another provider has already
456
- stopped the agent.
457
-
458
- #### Why the commit phase is bounded and the decision is not
459
-
460
- Waiting for unanimity forever is the one way this algorithm can strand an agent. The commit is
461
- durable and cannot be rolled back, so a provider that crashes, has its authentication revoked,
462
- or is uninstalled between granting and committing would hold Omni in `committing-break` with no
463
- exit: the providers that did commit have already stopped the agent, and the agent is neither
464
- working nor on a break.
465
-
466
- Unanimity is required for a reason that survives the bound. It exists so the agent is not stopped
467
- on one platform while another keeps routing work to them — and **a provider Omni cannot reach is
468
- routing nothing.** Setting it aside therefore costs none of the property it was protecting. Waiting
469
- for it costs the agent their break.
470
-
471
- Setting a provider aside is not a rollback and not a cancel. The commit stands and the
472
- obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
473
- snapshot before anything else, and the snapshot decides:
474
-
475
- - `on-break` or `starting-after-task` — the commit arrived after all. Nothing to send.
476
- - still `granted` — the commit was lost. Omni sends `commitBreak()` now. If the original turns up
477
- late behind it, the provider stops an agent who is already stopped; nothing happens, because a
478
- commit is a state to be in, not an act to be done.
479
- - `not-requested` — the grant did not survive. Omni makes a new request for that provider alone,
480
- against an agent who is already on break elsewhere. **A new login is this case too**: the grant
481
- belonged to the old session.
482
-
483
- The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
484
- no capacity until it is reconciled.
485
-
486
- **If the agent has already ended the break elsewhere, the break attempt is over** and the returning
487
- provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
488
- would stop an agent who is working again; `starting-after-task` or `on-break` gets `endBreak()`.
489
- Never rolling back is about a break that is still on, not one the agent has finished.
490
-
491
- Omni may tell the agent which platforms the break has not yet reached, as it already does when a
492
- break cannot be paired across every provider.
493
-
494
- The decision phase needs no such bound, because nothing has stopped. Work continues throughout
495
- `requesting-break`, so a provider that never answers costs the agent a wait rather than their
496
- availability, and the existing decision timeout resolves it by cancelling — which is safe precisely
497
- because no stop was ever committed.
498
-
499
- If cancel races with a late approval, the provider remains on the cancel path. If commit has already
500
- won, cancel returns `omni.break-already-committed`, and Omni resumes commit recovery rather than
501
- returning the agent to `working`.
502
-
503
- A forced break is not rolled back by this algorithm. If one appears during either the request or
504
- cancellation, the attempt is abandoned: Omni cancels every grant still outstanding and states
505
- `setCapacity({ count: 0 })` on every other usable provider, as **Forced breaks** sets out; no grant
506
- is committed, because the agent has already stopped everywhere.
507
-
508
- This is two-phase coordination across vendor systems: the approval phase keeps the agent working;
509
- the durable commit decision and snapshot reconciliation provide convergence after partial delivery.
510
-
511
- #### Reporting the break the agent is on
512
-
513
- `BreakState.activeReasonId` is the `BreakReason.id` the current break is on. Omni remembers what it
514
- asked for, but only until the session ends — after a reload or reconnect, or where the provider put
515
- the agent on the break itself, the provider is the only one who knows.
516
-
517
- Omit it when you cannot say, and when there is no break: reporting a reason alongside
518
- `status: "not-requested"` describes a break that is not happening, and is rejected.
519
-
520
- ### `cancelBreak()`
521
-
522
- Cancels the active pre-commit request while its status is `awaiting-approval` or `granted`.
523
- Cancellation releases the request but does not restore work because work never stopped. If
524
- commit already won, the provider returns `omni.break-already-committed`. The resulting state is
525
- reported through `break-state`.
526
-
527
- ### `commitBreak()`
528
-
529
- Commits the `granted` request. Once the provider has reported `granted`, it cannot fail for a
530
- business reason. On commit the provider stops offering new work and reports
531
- `starting-after-task` while existing work finishes, or `on-break` when the break is in effect.
532
- Committing a break that is already in effect changes nothing and answers `committed`.
533
-
534
- ### `endBreak()`
535
-
536
- Tells a provider that an agent already on a break wants to become available again. The provider
537
- reports the resulting state through `break-state` and provider status events.
538
-
539
- It ends the break, which is the thing that started. Nothing about the connection was ever paused,
540
- so there is nothing on it to resume.
253
+ **Capacity is not the agent's readiness.** It is the desk's division of one person across
254
+ providers, a ceiling on assignments at once, and it says nothing about the moment. An agent who
255
+ is about to finish a call and wants the next one lined up has a different thing to say, and says
256
+ it as **The agent's own queue** in `guide/queue.md` sets out; the ceiling stands unchanged while they do.
package/guide/lead.md CHANGED
@@ -32,6 +32,8 @@ computes nothing. See **`team-updated`** in `guide.md` and its companions.
32
32
  | `tasks` | The member's open tasks as `MemberTask`s: the same task the member's desk holds, less the workspace. The assignment, title, task type, phase, party, room, audio and completion terms -- `completionMode`, `wrapAllowance`, `wrapEndsInSeconds` -- are what the lead reads, and the task's record -- `history`, steps and totals -- travels whole, exactly as the member's desk holds it, so the lead's screen and the member's show the same record. The workspace -- `capabilities`, `capabilitySource`, `browsers` and their URLs -- is the member's desk's alone and never travels (`team.member.task.workspace`). The party's number and email are shown to leads or locked as the provider decides for leads, apart from what it decides for the member -- see **What the queue locks is locked on the whole task**. `[]` when the member holds none; omitted only where the provider cannot see them. |
33
33
  | `listening` | Every lead on this member's call — listening, coaching or joined — published to every lead alike: each entry names the lead by `leadId`, which of the member's calls by `assignmentId`, the `mode` they are heard in, and since when. The entry naming the signed-in lead is what their own audio opens on. Absent while nobody is on the call, never `[]` (`team.member.listening.*`); a lead is on one member's call at a time (`team.listening.single`), and a lead who is heard is also in the task's `onCall` (`team.member.listening.heard`). |
34
34
  | `request` | Present while the member is asking a lead to join one of their calls: which assignment, the note they wrote, since when. The call named is one their `tasks` carry (`team.member.request.assignment`). Absent when they are not asking. See **Lead assist**. |
35
+ | `nextCall`, `linedUp` | The member's own queue as their own snapshot carries it: their standing ask for the next call, and the call lined up for them. See **The agent's own queue** in `guide/queue.md`. |
36
+ | `phone` | The member's phone as the platform sees it: the same `PhoneState` the member's own snapshot carries, so a lead sees a desk phone that is down, or a call on it that is no task, as the member does. Present where the provider declares `phoneStatus`. See **The phone's own view** in `guide/phone.md`. |
35
37
  | `shift` | The member's own history for the day, about the person rather than any one call: `signedInAt`, `signedOutAt` once it has happened, today's totals as the provider counts them — `talkSeconds`, `holdSeconds`, `breakSeconds`, `tasksHandled`, each present only when the provider knows it — and the day's sign-in, sign-out and break `events`, oldest first. The same `Shift` the member's own snapshot carries, the same numbers from the same count. Omitted where the provider cannot say. |
36
38
 
37
39
  Each availability value means one thing:
package/guide/phone.md CHANGED
@@ -272,3 +272,68 @@ call, the same line in the ready state, so an agent does not learn of it from a
272
272
  whether work is held back is the adapter's decision from the report, as it is for every station
273
273
  fact. Where a provider's own platform holds a mute of its own -- a bridge that silences a leg --
274
274
  that is a fact about the switch, reported as the provider sees fit, and never the agent application's Mute.
275
+
276
+ ### The phone's own view
277
+
278
+ There are two views of a call, and the contract carries both. The task side says which calls are
279
+ the agent's work and where each stands: the phase, the audio, the room. The phone side is the
280
+ device: whether it can take a call at all, its own mute, and the calls on it, which exist whether
281
+ or not a task is behind them -- an internal call on the extension, a lead's listening leg, a call
282
+ the platform put on the phone that the desk never saw as a task. The host's report covers neither:
283
+ it says what the station has, a microphone and a speaker, and cannot say whether the phone is
284
+ registered. The platform sees the phone, so this is the provider's to publish, as `Snapshot.phone`
285
+ and `phone-updated`, from a manifest that declares `phoneStatus` (`snapshot.phone.required`,
286
+ `snapshot.phone.unexpected`, `event.phone.capability`).
287
+
288
+ ```ts
289
+ // a conference in progress on alloc-42 -- one channel, since the bridge mixes -- with alloc-43 parked
290
+ const busy: PhoneState = { phone: "deskPhone", status: "ready", muted: true, channels: [
291
+ { state: "active", since: "2026-09-12T10:30:04Z", assignmentId: "alloc-42" },
292
+ { state: "held", since: "2026-09-12T10:35:20Z", assignmentId: "alloc-43" },
293
+ ] };
294
+ // an internal call on the extension the desk never saw as a task
295
+ const internal: PhoneState = { phone: "deskPhone", status: "ready", channels: [{ state: "active", since: "2026-09-12T11:02:00Z" }] };
296
+ // a desk phone the platform lost
297
+ const down: PhoneState = { phone: "deskPhone", status: "unregistered", since: "2026-09-12T10:41:00Z", channels: [] };
298
+ ```
299
+
300
+ **The device.** `status` is the phone itself: `ready` is registered and usable, idle or busy, and
301
+ the other three are the states in which nothing can land, each wanting a different fix.
302
+
303
+ | `status` | Meaning |
304
+ | --- | --- |
305
+ | `ready` | Registered and usable, idle or busy. |
306
+ | `unregistered` | The platform has lost the phone: no registration from the extension, no route to the handset. Nothing can land, and no channel is on it (`phone.status.channels`). |
307
+ | `do-not-disturb` | The agent pressed it on the device. Nothing lands until they clear it; it is not a break, and the provider assigns or not as its platform does. |
308
+ | `off-hook` | The handset is up with no call on it, and no channel is on it. |
309
+
310
+ `phone` is the agent's device for this provider: the one the host chose for the login at
311
+ connect, from the manifest's phones, as the way the agent hears this provider's calls. A state
312
+ naming the other is the wrong phone (`phone.phone.mismatch`). The phone is the agent's; the login
313
+ is the agent's session with the provider, and what belongs to the login is that choice. `since` is when the status began, counted from the
314
+ provider's clock like every duration off the active call, and omitted rather than invented.
315
+
316
+ **The channels.** The phone has one active audio channel and any number held
317
+ (`phone.channel.active.single`), and a bridge is one channel: a conference is one entry, and its
318
+ members are on the task's `onCall`, never repeated here. Each channel is `ringing`, `active` or
319
+ `held`, since when, and names the task its call is where it is one. **Where a channel is a task's,
320
+ the two views agree** (`phone.channel.task`): `active` with the task at work and its audio
321
+ started, `held` with the task `paused`, `ringing` with the task `pending` or its party ringing on
322
+ `onCall`; and a task with its audio started has a channel (`phone.channel.missing`), since the
323
+ phone carries every call the desk is on. A channel naming no task is the phone's alone, and the
324
+ desk shows it as a call it cannot act on.
325
+
326
+ **The mute is the phone's, and it silences whatever is active.** It is one flag on the phone, not
327
+ a property of a channel: whichever channel is active is silent while it is set, and the held
328
+ channels are unaffected. Who owns it follows the phone. On a desk phone the mute button is the
329
+ handset's, and the platform observes it or does not: `muted: true` where it does, absent where it
330
+ does not, never invented. On a softphone the microphone is the host's, its mute is the host's
331
+ report as **The station is the agent application's** sets out, and nothing the provider publishes
332
+ moves it: `muted` on a softphone's state is the platform echoing the host, and is refused
333
+ (`phone.muted.softphone`). The `muted` step in the record stays what it is, the account of a leg,
334
+ never the live flag.
335
+
336
+ **The lead sees the member's phone as the member does.** The member on the team member list
337
+ carries the same `PhoneState`, republished with the member on every change, so a desk phone that
338
+ is down, or a call on it that is no task, shows the same on both screens, and neither derives it
339
+ from anything else.