@xema/omni-protocol 0.1.96 → 0.1.98
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/dist/index.d.ts +51 -64
- package/dist/index.js +13 -13
- package/dist/testing.d.ts +6 -7
- package/dist/testing.js +97 -122
- package/dist/validation.d.ts +2 -2
- package/dist/validation.js +76 -88
- package/guide.md +201 -197
- package/package.json +1 -1
package/guide.md
CHANGED
|
@@ -25,16 +25,17 @@ are used precisely throughout and mean nothing looser here.
|
|
|
25
25
|
| **Agent** | The person signed in and taking work. On `onCall`, `agent` is a person on the call by user id; transfer destinations may include an agent when the provider publishes that directory item. |
|
|
26
26
|
| **Lead** | An agent whose login declares `capabilities.team`. The provider publishes a `TeamMembers` object to them and to nobody else: **the login is the permission**. |
|
|
27
27
|
| **Local policy** | Rules configured by the agent application about this agent, outside the provider protocol and never sent to a provider. It gates whether an offer may be rejected, whether the agent goes ready on login, and whether tasks are auto-accepted. Where a capability and local policy disagree, the stricter wins. |
|
|
28
|
-
| **Call** | The caller’s complete phone call, which may continue through IVRs, queues and several agents. |
|
|
29
|
-
| **
|
|
30
|
-
| **
|
|
28
|
+
| **Call** | The caller’s complete phone call, which may continue through IVRs, queues and several agents. The protocol never describes the IVR or the queue; it carries the call's history and details to whichever agent holds it now. |
|
|
29
|
+
| **Assignment** | The call routed to this agent, from the offer until the task ends. It is the one thing that crosses: the provider assigns, names the assignment with `assignmentId`, and that is the only name the desk ever uses back. A call that comes back is a new assignment on the same call, and a declined or lapsed offer is an assignment that became nothing more. What the platform calls its own record, and whether it reuses that name, is the adapter's business and unknown to the desk. |
|
|
30
|
+
| **Interaction** | The agent's time on the call, from answer until their audio ends: the `in-progress` and `paused` phases. An assignment produces at most one interaction, and a call that returns produces a new assignment and so a new interaction. |
|
|
31
|
+
| **Task** | The desk's record of one assignment, from offer through wrap: the workspace, controls and completion work the agent has for it, described by the provider and held by the desk. A task is identified by the assignment it is the record of. `pending`, `confirmed` and `preview` are assigned and not yet interacting; `in-progress` and `paused` are the interaction; `completing` is the assignment outliving its interaction. A voice task is one assignment, never the caller's whole call. |
|
|
31
32
|
| **Channel** | The kind of work a provider carries: `voice`, `chat`, or `email`. Fixed per provider by its manifest. |
|
|
32
33
|
| **Task type** | The provider's own name for a category of work — a queue, a mailbox folder, a chat source. Free-form, and finer-grained than a channel. |
|
|
33
34
|
| **Capability** | A provider's declaration that a control exists for a task or a session. It says *offer this*; the provider performs it, and the agent application only offers it — see **Where a command executes**. |
|
|
34
35
|
| **Login** | One authenticated sign-in to one provider, identified by `loginId`. A transport reconnect keeps it; signing in again replaces it, and nothing tied to the old `loginId` survives. |
|
|
35
36
|
| **Transport** | The adapter's connection to its platform: a WebSocket or SignalR connection, required to be persistent and ordered. Which one and how it reconnects are the adapter's business; losing it does not end a login. |
|
|
36
37
|
| **Connection** | The `Connection` object Omni holds for one login: the methods it can call and the events it receives. |
|
|
37
|
-
| **Concurrent capacity** | How many tasks this provider may have
|
|
38
|
+
| **Concurrent capacity** | How many tasks this provider may have assigned to the agent at once — an absolute ceiling, stated as `AgentCapacity.count` and standing until Omni restates it. The provider counts its own outstanding tasks against it. |
|
|
38
39
|
| **Snapshot** | The provider's complete state at one moment. It replaces what Omni holds; it is never a patch. |
|
|
39
40
|
| **Event** | One completed transaction reported after a snapshot established the baseline. |
|
|
40
41
|
| **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
|
|
@@ -104,8 +105,7 @@ the semantic name in every contract field rather than repeating the primitive ty
|
|
|
104
105
|
```ts
|
|
105
106
|
type IsoTimestamp = string;
|
|
106
107
|
type UserId = string;
|
|
107
|
-
type
|
|
108
|
-
type AllocationId = string;
|
|
108
|
+
type AssignmentId = string;
|
|
109
109
|
type DialId = string;
|
|
110
110
|
type DurationSeconds = number;
|
|
111
111
|
```
|
|
@@ -114,7 +114,7 @@ type DurationSeconds = number;
|
|
|
114
114
|
| --- | --- | --- |
|
|
115
115
|
| `IsoTimestamp` | `string` | An RFC-3339 timestamp with `Z` or an explicit numeric offset. Timezone-less values are invalid. It must pass the shared runtime validator. A JavaScript `Date` never crosses the protocol boundary. |
|
|
116
116
|
| `UserId` | `string` | A non-empty, opaque, stable identifier for a person, **issued by the provider** and drawn from the same directory as `AuthenticationState.identity.id`. It names agents and managers alike; the role is established by where the value appears, not by its type. Compare it exactly and only within one provider; do not parse it or infer meaning from its format. |
|
|
117
|
-
| `
|
|
117
|
+
| `AssignmentId` | `string` | A non-empty, opaque identifier for one assignment, **issued by the provider**: unique within the provider and never reused while the provider is still speaking about it. Whether it is the platform's own handle passed through or one the adapter minted is the adapter's business. Omni scopes it with the provider ID -- see `assignmentKey()`. |
|
|
118
118
|
| `DurationSeconds` | `number` | A non-negative integer duration measured in seconds. |
|
|
119
119
|
|
|
120
120
|
### There is no Omni-wide user identity
|
|
@@ -124,10 +124,10 @@ identifier of its own for an agent or a manager, and none crosses this boundary
|
|
|
124
124
|
operating-system account, not a directory identity, not a licence.
|
|
125
125
|
|
|
126
126
|
So a `UserId` means nothing outside the provider that issued it. Provider A's
|
|
127
|
-
`
|
|
127
|
+
`history[].by` and provider B's team member list `memberId` are unrelated strings that will
|
|
128
128
|
eventually collide, and one person on several providers has several identities that nothing here
|
|
129
129
|
pairs. Scope every user identifier with its provider ID before storing or comparing it, exactly as
|
|
130
|
-
`
|
|
130
|
+
`assignmentKey()` already does for tasks — see `userKey()` under **Utilities**.
|
|
131
131
|
|
|
132
132
|
## Shapes
|
|
133
133
|
|
|
@@ -327,8 +327,7 @@ type RecordingCommand = {
|
|
|
327
327
|
| { action: "pause" | "resume" | "stop" | "cancel"; recordingId: string }
|
|
328
328
|
);
|
|
329
329
|
interface HostRecordingReport {
|
|
330
|
-
|
|
331
|
-
allocationId: AllocationId;
|
|
330
|
+
assignmentId: AssignmentId;
|
|
332
331
|
state: RecordingState;
|
|
333
332
|
}
|
|
334
333
|
interface HostRecording {
|
|
@@ -339,8 +338,7 @@ interface HostRecording {
|
|
|
339
338
|
}
|
|
340
339
|
type RecordingCommandResult = Exclude<TaskCommandResult, { status: "dialling" }>;
|
|
341
340
|
interface HostRecordingRequest {
|
|
342
|
-
|
|
343
|
-
allocationId: AllocationId;
|
|
341
|
+
assignmentId: AssignmentId;
|
|
344
342
|
command: RecordingCommand & { source: "host" };
|
|
345
343
|
}
|
|
346
344
|
|
|
@@ -465,15 +463,15 @@ type SetPreferenceRequest =
|
|
|
465
463
|
| { id: PreferenceId; enabled: boolean }
|
|
466
464
|
| { id: PreferenceId; inherit: true };
|
|
467
465
|
|
|
468
|
-
type
|
|
466
|
+
type HistoryReport = { assignmentId: AssignmentId; at: IsoTimestamp } & (
|
|
469
467
|
| { step: "muted"; mutedBy: MutedBy }
|
|
470
|
-
| { step: Exclude<
|
|
468
|
+
| { step: Exclude<HistoryStep, "muted">; mutedBy?: never }
|
|
471
469
|
) & (
|
|
472
470
|
| { ended: true; seconds: DurationSeconds }
|
|
473
471
|
| { ended?: never; seconds?: DurationSeconds }
|
|
474
472
|
);
|
|
475
473
|
|
|
476
|
-
type
|
|
474
|
+
type HistoryReportResult =
|
|
477
475
|
| { status: "recorded"; at: IsoTimestamp }
|
|
478
476
|
| { status: "failed"; failure: ProtocolFailure };
|
|
479
477
|
|
|
@@ -589,7 +587,7 @@ The channel arms are why `Task<"email">` rejects `hold` at compile time rather t
|
|
|
589
587
|
|
|
590
588
|
```ts
|
|
591
589
|
const BROWSER_ISOLATION_SCHEMES = {
|
|
592
|
-
|
|
590
|
+
PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME: "ProviderName.AssignmentId.TabName",
|
|
593
591
|
TAB_NAME: "TabName",
|
|
594
592
|
PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME: "ProviderName.TaskTypeName.TabName",
|
|
595
593
|
PROVIDER_NAME__TAB_NAME: "ProviderName.TabName",
|
|
@@ -618,8 +616,7 @@ type PersonalBrowser = Browser;
|
|
|
618
616
|
|
|
619
617
|
type BrowserSessionKeyInput = {
|
|
620
618
|
providerId: string;
|
|
621
|
-
|
|
622
|
-
allocationId: AllocationId;
|
|
619
|
+
assignmentId: AssignmentId;
|
|
623
620
|
taskType: string;
|
|
624
621
|
browser: TaskBrowser;
|
|
625
622
|
};
|
|
@@ -652,7 +649,7 @@ type TaskAttribute = TaskAttributeBase & (
|
|
|
652
649
|
| { type: "timestamp"; at: IsoTimestamp }
|
|
653
650
|
);
|
|
654
651
|
|
|
655
|
-
type
|
|
652
|
+
type HistoryStep =
|
|
656
653
|
| "queued"
|
|
657
654
|
| "offered"
|
|
658
655
|
| "answered"
|
|
@@ -662,16 +659,16 @@ type InteractionStep =
|
|
|
662
659
|
| "conferenced"
|
|
663
660
|
| "unanswered";
|
|
664
661
|
|
|
665
|
-
type
|
|
666
|
-
steps:
|
|
662
|
+
type TaskHistory = {
|
|
663
|
+
steps: TaskHistoryStep[];
|
|
667
664
|
interactionSeconds?: DurationSeconds;
|
|
668
665
|
holdSeconds?: DurationSeconds;
|
|
669
666
|
queueSeconds?: DurationSeconds;
|
|
670
667
|
transfers?: number;
|
|
671
668
|
};
|
|
672
669
|
|
|
673
|
-
type
|
|
674
|
-
step:
|
|
670
|
+
type TaskHistoryStep = {
|
|
671
|
+
step: HistoryStep;
|
|
675
672
|
at: IsoTimestamp;
|
|
676
673
|
dialId?: DialId;
|
|
677
674
|
destinationId?: string;
|
|
@@ -702,14 +699,14 @@ type TaskLeadAssist = { note?: string; since: IsoTimestamp } & (
|
|
|
702
699
|
|
|
703
700
|
type TaskAssisting = {
|
|
704
701
|
memberId: UserId;
|
|
702
|
+
assignmentId: AssignmentId;
|
|
705
703
|
note?: string;
|
|
706
704
|
since: IsoTimestamp;
|
|
707
705
|
};
|
|
708
706
|
|
|
709
707
|
type TaskListening = {
|
|
710
708
|
memberId: UserId;
|
|
711
|
-
|
|
712
|
-
allocationId: AllocationId;
|
|
709
|
+
assignmentId: AssignmentId;
|
|
713
710
|
mode: ListeningMode;
|
|
714
711
|
since: IsoTimestamp;
|
|
715
712
|
};
|
|
@@ -733,8 +730,7 @@ type TaskMediaState = "started" | "ended";
|
|
|
733
730
|
type CapabilitySource = "queue" | "ungoverned" | "undetermined";
|
|
734
731
|
|
|
735
732
|
type Task<C extends Channel = Channel> = {
|
|
736
|
-
|
|
737
|
-
allocationId: AllocationId;
|
|
733
|
+
assignmentId: AssignmentId;
|
|
738
734
|
title: string;
|
|
739
735
|
channel: C;
|
|
740
736
|
taskType: string;
|
|
@@ -748,7 +744,7 @@ type Task<C extends Channel = Channel> = {
|
|
|
748
744
|
atDeadline?: PreviewDeadline;
|
|
749
745
|
reference?: string;
|
|
750
746
|
attributes?: TaskAttribute[];
|
|
751
|
-
|
|
747
|
+
history?: TaskHistory;
|
|
752
748
|
} & TaskCompletion & (
|
|
753
749
|
C extends "voice"
|
|
754
750
|
? { recording?: { provider?: RecordingState }; onCall?: OnCall[]; leadAssist?: TaskLeadAssist; assisting?: TaskAssisting; listening?: TaskListening; media?: TaskMediaState }
|
|
@@ -840,8 +836,7 @@ type TaskCommand<C extends Channel = Channel> =
|
|
|
840
836
|
| CustomTaskCommand;
|
|
841
837
|
|
|
842
838
|
type TaskCommandRequest<C extends Channel = Channel> = {
|
|
843
|
-
|
|
844
|
-
allocationId: AllocationId;
|
|
839
|
+
assignmentId: AssignmentId;
|
|
845
840
|
command: TaskCommand<C>;
|
|
846
841
|
};
|
|
847
842
|
|
|
@@ -1001,8 +996,7 @@ type TeamMember = {
|
|
|
1001
996
|
type LeadRequest = {
|
|
1002
997
|
id: string;
|
|
1003
998
|
memberId: UserId;
|
|
1004
|
-
|
|
1005
|
-
allocationId: AllocationId;
|
|
999
|
+
assignmentId: AssignmentId;
|
|
1006
1000
|
note?: string;
|
|
1007
1001
|
since: IsoTimestamp;
|
|
1008
1002
|
};
|
|
@@ -1081,8 +1075,7 @@ type OpenMediaResult =
|
|
|
1081
1075
|
| { status: "unavailable"; failure: ProtocolFailure };
|
|
1082
1076
|
|
|
1083
1077
|
type OpenMediaRequest = {
|
|
1084
|
-
|
|
1085
|
-
allocationId: AllocationId;
|
|
1078
|
+
assignmentId: AssignmentId;
|
|
1086
1079
|
localAudio?: MediaStream;
|
|
1087
1080
|
};
|
|
1088
1081
|
```
|
|
@@ -1110,16 +1103,16 @@ type ProviderEvent =
|
|
|
1110
1103
|
| {
|
|
1111
1104
|
type: "task-offered";
|
|
1112
1105
|
task: Task;
|
|
1113
|
-
|
|
1106
|
+
assignmentExpiresAt?: IsoTimestamp;
|
|
1114
1107
|
}
|
|
1115
1108
|
| { type: "task-updated"; task: Task }
|
|
1116
|
-
| { type: "task-media-started";
|
|
1117
|
-
| { type: "task-media-ended";
|
|
1118
|
-
| { type: "task-ended";
|
|
1119
|
-
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome;
|
|
1109
|
+
| { type: "task-media-started"; assignmentId: AssignmentId }
|
|
1110
|
+
| { type: "task-media-ended"; assignmentId: AssignmentId }
|
|
1111
|
+
| { type: "task-ended"; assignmentId: AssignmentId; outcome: TaskOutcome }
|
|
1112
|
+
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; assignmentId?: AssignmentId; destinationId?: string; reason?: string }
|
|
1120
1113
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
1121
1114
|
| { type: "queue-summary"; summary: QueueSummary }
|
|
1122
|
-
| { type: "diagnostic"; expected: string; observed: string;
|
|
1115
|
+
| { type: "diagnostic"; expected: string; observed: string; assignmentId?: AssignmentId }
|
|
1123
1116
|
| { type: "team-updated"; team: TeamMembers }
|
|
1124
1117
|
| { type: "contacts-updated"; contacts: Contact[] }
|
|
1125
1118
|
| { type: "calendar-updated"; scheduledActivities: ScheduledActivity[] };
|
|
@@ -1181,7 +1174,7 @@ type Connection<C extends Channel = Channel> = {
|
|
|
1181
1174
|
executeTeamListen?(request: TeamListenCommandRequest): Promise<TeamCommandResult>;
|
|
1182
1175
|
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
1183
1176
|
setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
|
|
1184
|
-
recordStep?(report:
|
|
1177
|
+
recordStep?(report: HistoryReport): Promise<HistoryReportResult>;
|
|
1185
1178
|
executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
|
|
1186
1179
|
};
|
|
1187
1180
|
|
|
@@ -1229,7 +1222,7 @@ const BREAK_KINDS = [
|
|
|
1229
1222
|
|
|
1230
1223
|
type BreakKind = (typeof BREAK_KINDS)[number];
|
|
1231
1224
|
|
|
1232
|
-
const
|
|
1225
|
+
const HISTORY_STEPS_WITH_A_PERSON = [
|
|
1233
1226
|
"offered",
|
|
1234
1227
|
"answered",
|
|
1235
1228
|
"held",
|
|
@@ -1242,7 +1235,7 @@ const INTERACTION_STEPS_WITH_A_PERSON = [
|
|
|
1242
1235
|
const OMNI_FAILURE_CODES = [
|
|
1243
1236
|
"omni.not-authenticated",
|
|
1244
1237
|
"omni.capability-not-enabled",
|
|
1245
|
-
"omni.
|
|
1238
|
+
"omni.assignment-not-found",
|
|
1246
1239
|
"omni.destination-not-permitted",
|
|
1247
1240
|
"omni.phone-not-permitted",
|
|
1248
1241
|
"omni.rate-limited",
|
|
@@ -1252,8 +1245,8 @@ const OMNI_FAILURE_CODES = [
|
|
|
1252
1245
|
type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
1253
1246
|
```
|
|
1254
1247
|
|
|
1255
|
-
`
|
|
1256
|
-
takes part in. `
|
|
1248
|
+
`HISTORY_STEPS_WITH_A_PERSON` is every `HistoryStep` except `queued`, which is the one nobody
|
|
1249
|
+
takes part in. `historyStepExpectsAPerson()` tests membership.
|
|
1257
1250
|
|
|
1258
1251
|
### Failure and validation
|
|
1259
1252
|
|
|
@@ -1462,8 +1455,8 @@ is never the provider's answer: an answer travels only as a resolved `{ status:
|
|
|
1462
1455
|
and every rejection is transport loss, whatever the last `transport-status` said and whatever the
|
|
1463
1456
|
rejection carries.
|
|
1464
1457
|
|
|
1465
|
-
A command therefore carries no key. The provider names
|
|
1466
|
-
member
|
|
1458
|
+
A command therefore carries no key. The provider names the assignment, the lead request and the
|
|
1459
|
+
member, and Omni refers to them by those names; **Omni never asks a provider to remember a name
|
|
1467
1460
|
Omni made up.**
|
|
1468
1461
|
|
|
1469
1462
|
A command whose second execution changes nothing needs no protection: committing a break that is
|
|
@@ -1485,11 +1478,11 @@ fired into a state that has moved on.
|
|
|
1485
1478
|
|
|
1486
1479
|
### 7. Work is pulled, never pushed
|
|
1487
1480
|
|
|
1488
|
-
|
|
1481
|
+
Assign only within the concurrent capacity Omni has stated for this agent.
|
|
1489
1482
|
|
|
1490
|
-
An
|
|
1483
|
+
An assignment beyond that capacity, or with none currently stated, is invalid and Omni rejects it
|
|
1491
1484
|
as a protocol violation. Work the agent was already working on when the connection came back is
|
|
1492
|
-
reported in the snapshot; it is not an
|
|
1485
|
+
reported in the snapshot; it is not an assignment.
|
|
1493
1486
|
|
|
1494
1487
|
### 8. State is authoritative at the provider
|
|
1495
1488
|
|
|
@@ -1499,7 +1492,7 @@ replaces its local provider view; it does not overwrite the provider.
|
|
|
1499
1492
|
|
|
1500
1493
|
### 9. Define a vocabulary; do not merely list it
|
|
1501
1494
|
|
|
1502
|
-
Every member of a closed set — break kinds,
|
|
1495
|
+
Every member of a closed set — break kinds, history steps, destination kinds — must have a
|
|
1503
1496
|
normative definition; matching names alone do not establish shared meaning.
|
|
1504
1497
|
|
|
1505
1498
|
### 10. Order on the wire is display order
|
|
@@ -1538,7 +1531,7 @@ Request/response polling does not have those properties and is not a transport f
|
|
|
1538
1531
|
### 3. An adapter's memory is the platform's and the login's, never the connection's
|
|
1539
1532
|
|
|
1540
1533
|
Everything an adapter publishes about a task is derived from what the platform states, or kept in
|
|
1541
|
-
the login's store where the platform cannot hold it. Per offer, the `
|
|
1534
|
+
the login's store where the platform cannot hold it. Per offer, the `assignmentId` comes from what
|
|
1542
1535
|
the offer itself states; per call, the media follows `task-media-started` and `task-media-ended`,
|
|
1543
1536
|
never a flag set when the audio was opened; per session, the phone the agent holds is what the
|
|
1544
1537
|
platform restates on activation, not a mapping read once over HTTP. What the platform cannot hold
|
|
@@ -1547,11 +1540,11 @@ platform restates on activation, not a mapping read once over HTTP. What the pla
|
|
|
1547
1540
|
A fact read once over a connection and kept in the adapter object dies with the client. An agent application
|
|
1548
1541
|
reload is the first client dying and a second coming up for the same login, and the second
|
|
1549
1542
|
publishes the same truth about the same task as the first did, or the first was publishing
|
|
1550
|
-
something it made up: an
|
|
1543
|
+
something it made up: an assignment minted for the connection, media remembered as a boolean, a
|
|
1551
1544
|
phone the second never read. Three pieces of exactly that state were found in one adapter in an
|
|
1552
1545
|
afternoon, and none of it was visible until the adapter was built twice. The drive's `rebuild` is the test of this
|
|
1553
1546
|
principle: given a way to build the adapter again, the run hands the login over and holds the
|
|
1554
|
-
second to what the first published (`drive.reload.
|
|
1547
|
+
second to what the first published (`drive.reload.snapshot`, `.history`, `.openMedia`, and the
|
|
1555
1548
|
stream's own rules across the resync). An adapter that cannot be built twice against its platform
|
|
1556
1549
|
and publish the same task is not a conformant adapter yet, whatever a single run says.
|
|
1557
1550
|
|
|
@@ -1959,7 +1952,7 @@ short-lived, opaque `flowId`; rejection returns `AuthenticationFailure`.
|
|
|
1959
1952
|
|
|
1960
1953
|
#### Browser SSO
|
|
1961
1954
|
|
|
1962
|
-
For `browser-sso`, Omni
|
|
1955
|
+
For `browser-sso`, Omni issues a one-time callback URL and passes it to `start()`:
|
|
1963
1956
|
|
|
1964
1957
|
```ts
|
|
1965
1958
|
{
|
|
@@ -2028,7 +2021,7 @@ failure:
|
|
|
2028
2021
|
```
|
|
2029
2022
|
|
|
2030
2023
|
The `User` it carries is the **root of this provider's user namespace**. Every other person this
|
|
2031
|
-
provider names — a team member, the manager on a forced break, the agent on
|
|
2024
|
+
provider names — a team member, the manager on a forced break, the agent on a history step —
|
|
2032
2025
|
is identified from the same directory and carries the same `UserId` type.
|
|
2033
2026
|
|
|
2034
2027
|
| Field | Contract |
|
|
@@ -2080,7 +2073,7 @@ Creates one live provider connection for the signed-in agent.
|
|
|
2080
2073
|
| `autoAcceptTasks` | Agent local policy policy relayed to the provider at login, stated by the agent application on every connection and never assumed from its absence. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
|
|
2081
2074
|
| `timeZone` | The same value passed as `AuthenticationContext.timeZone`. The provider stores it on the agent and carries it on the identity. See **The agent's day**. |
|
|
2082
2075
|
| `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's agent application states what its Mute does, and a desk-phone or conversation login's agent application cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the agent application's**. |
|
|
2083
|
-
| `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry
|
|
2076
|
+
| `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry its assignment id and go with the task**: a key written about a task names the assignment id in the key, and is deleted before the task's end is published, so that nothing of a closed task survives its ending and nothing is left for whatever comes next -- a record with legs the agent application never reported against it. A login-scoped key carries no assignment id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`drive.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`drive.store.late`). The ordinary late write is the agent application's, not the adapter's: an agent application reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.assignment-not-found` and writes nothing -- the agent application ends its own open legs at the task's end, so such a report is the agent application's error to see, and an agent application reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
|
|
2084
2077
|
| `host` | The agent application's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The agent application reports, the adapter decides**. |
|
|
2085
2078
|
| `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
|
|
2086
2079
|
| `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
|
|
@@ -2156,9 +2149,9 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
2156
2149
|
| `subscribe(listener)` | Always. |
|
|
2157
2150
|
| `disconnect()` | Always. |
|
|
2158
2151
|
| `refused(report)` | Always. The agent application tells the adapter what it would not take -- a snapshot it did not replace its state with, an event it dropped -- with every rule broken, so a refusal is visible on both sides. See **What the agent application does with what it refuses**. |
|
|
2159
|
-
| `setCapacity(capacity)` | Always. Nothing may be
|
|
2152
|
+
| `setCapacity(capacity)` | Always. Nothing may be assigned until a capacity is stated, so there is no connection that does not receive it. |
|
|
2160
2153
|
| `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
|
|
2161
|
-
| `getUserDetails(ids)` | The adapter publishes any `UserId`: on `ForcedBreak.by`, a team member list, or `
|
|
2154
|
+
| `getUserDetails(ids)` | The adapter publishes any `UserId`: on `ForcedBreak.by`, a team member list, or `history[].by`. Each `User` carries its `timeZone`; a person whose zone the provider cannot name is omitted from the answer, as any unresolvable id is. |
|
|
2162
2155
|
| `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
|
|
2163
2156
|
| `requestBreak(request)` | The login declares `capabilities.breaks`. |
|
|
2164
2157
|
| `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
|
|
@@ -2217,7 +2210,7 @@ getUserDetails(ids: UserId[]): Promise<User[]>
|
|
|
2217
2210
|
```
|
|
2218
2211
|
|
|
2219
2212
|
Required of any adapter that publishes a `UserId` — on `ForcedBreak.by`, a team member list, or
|
|
2220
|
-
`
|
|
2213
|
+
`history[].by`. Publishing an identifier Omni cannot resolve puts a name on screen
|
|
2221
2214
|
that reads as a database key.
|
|
2222
2215
|
|
|
2223
2216
|
- **Omit an id you cannot resolve; do not invent a name for it.** A missing entry says *I do not
|
|
@@ -2231,7 +2224,7 @@ that reads as a database key.
|
|
|
2231
2224
|
particular value, or on Omni asking again at any particular moment.
|
|
2232
2225
|
|
|
2233
2226
|
This is the only place a name comes from. Task data carries identifiers alone —
|
|
2234
|
-
`
|
|
2227
|
+
`history[].by` is an id and nothing more — so a name is never copied into a task, never
|
|
2235
2228
|
duplicated across tasks, and never stale.
|
|
2236
2229
|
|
|
2237
2230
|
### `Connection.disconnect()`
|
|
@@ -2243,12 +2236,12 @@ Stops the connection and releases adapter-owned resources.
|
|
|
2243
2236
|
- Must remove event handlers and release media resources owned by the adapter.
|
|
2244
2237
|
- Does not imply that active tasks were completed or removed.
|
|
2245
2238
|
|
|
2246
|
-
## Task
|
|
2239
|
+
## Task assignment lifecycle
|
|
2247
2240
|
|
|
2248
|
-
The task-
|
|
2241
|
+
The task-assignment lifecycle has five ordered stages:
|
|
2249
2242
|
|
|
2250
2243
|
**1. The agent signs in.** Nothing on the wire says ready or not-ready: the agent is not ready
|
|
2251
|
-
until the agent application has stated a capacity, and ready once it has. A provider
|
|
2244
|
+
until the agent application has stated a capacity, and ready once it has. A provider assigns nothing before a
|
|
2252
2245
|
capacity is stated, and needs no other word for it.
|
|
2253
2246
|
|
|
2254
2247
|
**2. The agent becomes ready.** The agent application states the capacity when its local policy says: at once,
|
|
@@ -2318,10 +2311,10 @@ automatic or requires the agent. A provider that requires automatic acceptance s
|
|
|
2318
2311
|
```ts
|
|
2319
2312
|
declare const task: Task;
|
|
2320
2313
|
|
|
2321
|
-
const
|
|
2314
|
+
const assignment = {
|
|
2322
2315
|
type: "task-offered",
|
|
2323
2316
|
task: { ...task, phase: "pending", acceptance: "consent" },
|
|
2324
|
-
|
|
2317
|
+
assignmentExpiresAt: "2026-08-25T10:41:07.000Z",
|
|
2325
2318
|
} satisfies Extract<ProviderEvent, { type: "task-offered" }>;
|
|
2326
2319
|
```
|
|
2327
2320
|
|
|
@@ -2339,7 +2332,7 @@ on a desk phone — and is not in question per call. See **How the agent hears t
|
|
|
2339
2332
|
|
|
2340
2333
|
Automatic acceptance still begins with `task-offered`.
|
|
2341
2334
|
|
|
2342
|
-
`
|
|
2335
|
+
`assignmentExpiresAt` is the deadline after which the offer lapses. Where present, Omni counts
|
|
2343
2336
|
down and stops offering **Accept** once it passes; the provider ends the lapsed offer with
|
|
2344
2337
|
`task-ended` and an `expired` outcome naming `pending`, since nobody cancelled it. **Omit it unless the provider can observe it.**
|
|
2345
2338
|
A provider that reports only elapsed ring time after the fact cannot say when an offer is due
|
|
@@ -2353,7 +2346,7 @@ A provider may withdraw a pending task by emitting `task-ended` with a `cancelle
|
|
|
2353
2346
|
`agent` for a decline, `provider` for a withdrawal or a re-route, `party` for a caller who abandoned
|
|
2354
2347
|
the ring. One word for the three left a supervisor's record unable to tell them apart, so `by` is
|
|
2355
2348
|
required and closed (`event.taskEnded.outcome.cancelled.by`). An offer nobody acted on before
|
|
2356
|
-
`
|
|
2349
|
+
`assignmentExpiresAt` is not cancelled by anyone: it lapses, and ends `expired` naming `pending`.
|
|
2357
2350
|
|
|
2358
2351
|
### Tasks already in progress
|
|
2359
2352
|
|
|
@@ -2361,7 +2354,7 @@ The provider does not introduce new work as already `in-progress`.
|
|
|
2361
2354
|
|
|
2362
2355
|
A task appears already `in-progress` only in a snapshot taken after a reconnect or a resync,
|
|
2363
2356
|
reporting work that began earlier in this login and never stopped. A fresh login has none to
|
|
2364
|
-
report: nothing has been
|
|
2357
|
+
report: nothing has been assigned yet, and work the agent was working elsewhere is not carried
|
|
2365
2358
|
into a new session.
|
|
2366
2359
|
|
|
2367
2360
|
## Tasks
|
|
@@ -2378,8 +2371,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2378
2371
|
|
|
2379
2372
|
| Field | Contract |
|
|
2380
2373
|
| --- | --- |
|
|
2381
|
-
| `
|
|
2382
|
-
| `allocationId` | Required `AllocationId`: this life of the task, minted once per offer and never reused for the life of the login, whatever the id does. Every event, command and report that names a task names its allocation too, so a late dial outcome or a late interaction report for the first customer never lands on the next under the same id. The stream refuses an offer reusing one (`stream.taskOffered.allocation`), an update restating another life of the id (`stream.taskUpdated.allocation`), and a media or ending event naming a life that has ended or that nobody has seen (`stream.allocation.ended`, `.unknown`); a `dial-outcome` may name an ended life, since a dial placed late routinely outlives its call, and the agent application routes it there. See **A task's life on the wire**. |
|
|
2374
|
+
| `assignmentId` | Required `AssignmentId`: the assignment this task is the record of, and its one identity. Issued by the provider, unique within the provider, never reused. Every event, command and report that names a task names it by this, so a late dial outcome or a late history report for one customer never lands on the next. The stream refuses an assignment introduced twice (`stream.taskOffered.duplicate`) and a media or ending event naming one that has ended or that nobody has seen (`stream.assignment.ended`, `.unknown`); a `dial-outcome` may name an ended assignment, since a dial placed late routinely outlives its call, and the agent application routes it there. See **A task's life on the wire**. |
|
|
2383
2375
|
| `title` | Agent-facing task title. |
|
|
2384
2376
|
| `channel` | Channel used for this task. It must equal the source provider's manifest channel. |
|
|
2385
2377
|
| `taskType` | Required provider-defined source or category of work, such as a voice `Queue Name`, `Mailbox Folder`, `Chat Source`, `Support`, `Billing`, or `Returns`. |
|
|
@@ -2396,10 +2388,10 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2396
2388
|
| `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
|
|
2397
2389
|
| `wrapAllowance` | Fixed time allowed to complete the task after primary interaction ends. For real-time media, it begins after `task-media-ended`. Required under `provider-automatic`, where the provider acts on it. Optional under `agent-command`: omitted says the provider imposes no deadline, and Omni counts nothing down. |
|
|
2398
2390
|
| `attributes` | Optional ordered, typed `TaskAttribute` entries with keys unique within the task. Each contact or timestamp is a separate array item; new attribute shapes require new union members. |
|
|
2399
|
-
| `
|
|
2391
|
+
| `history` | The call record: `steps` — the ordered interaction history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `interactionSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **Interaction history**. |
|
|
2400
2392
|
| `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer -- carrying a `stage` while being dialled again on the same task, a connect-back with the agent application's `dialId` or a platform's callback without, ringing from the moment the dial is placed and joined on its answered outcome --, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destinationId` dialled, the `dialId` where an agent application placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the agent application supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2401
2393
|
| `leadAssist` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Lead assist**. |
|
|
2402
|
-
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked,
|
|
2394
|
+
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, the member's `assignmentId`, and their note. The lead's task has an assignment of its own, so the member's is named, or the copy no longer says which call it joined. Its presence is what makes `lead-assist` `take-over-call` and `leave` issuable. See **Lead assist**. |
|
|
2403
2395
|
| `listening` | Voice only, on the lead's own task while they listen to a member's call: whose call, which call, and the `mode` they are heard in, restated on every change. Never on the member's task, and never together with `assisting`. See **Listening to a call**. |
|
|
2404
2396
|
|
|
2405
2397
|
`TaskAttribute` entries carry typed detail alongside the task:
|
|
@@ -2428,15 +2420,15 @@ The canonical task transitions are:
|
|
|
2428
2420
|
|
|
2429
2421
|
| From | Decision or event | To |
|
|
2430
2422
|
| --- | --- | --- |
|
|
2431
|
-
| No task | Provider
|
|
2423
|
+
| No task | Provider assigns a task | `pending` |
|
|
2432
2424
|
| `pending` | Task is accepted | `confirmed` |
|
|
2433
2425
|
| `confirmed` | The customer's record is put in front of the agent before any call goes out (voice) | `preview` |
|
|
2434
2426
|
| `confirmed` | Work begins | `in-progress` |
|
|
2435
2427
|
| `preview` | Agent presses Call (`call`) and the customer answers, or the deadline `calls` and they answer | `in-progress` |
|
|
2436
2428
|
| `preview` | The call goes out and nobody answers | `completing` |
|
|
2437
2429
|
| `preview` | The preparation target passes with `atDeadline: "waits"` | Remains `preview`, waiting for the agent to press Call |
|
|
2438
|
-
| `pending` | Provider withdraws the
|
|
2439
|
-
| `pending` | The offer lapses at `
|
|
2430
|
+
| `pending` | Provider withdraws the assignment, the agent declines, or the party abandons the ring | Removed by `task-ended` with `cancelled` outcome, `by` saying which |
|
|
2431
|
+
| `pending` | The offer lapses at `assignmentExpiresAt` | Removed by `task-ended` with `expired` outcome naming `pending` |
|
|
2440
2432
|
| No task | Snapshot reports work already underway | `in-progress` |
|
|
2441
2433
|
| `in-progress` | Provider or agent pauses the task | `paused` |
|
|
2442
2434
|
| `in-progress` | Agent starts a warm transfer (`transfer` `warm`); the customer is parked | `paused` |
|
|
@@ -2448,23 +2440,26 @@ The canonical task transitions are:
|
|
|
2448
2440
|
|
|
2449
2441
|
#### A task's life on the wire
|
|
2450
2442
|
|
|
2451
|
-
A task
|
|
2452
|
-
|
|
2453
|
-
one
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2443
|
+
A task has one name, the assignment it is the record of, and the provider issues it. Platforms
|
|
2444
|
+
reuse their own handles: a closed one is retired minutes later, a requeue takes seconds, and on
|
|
2445
|
+
one platform a call was offered twice, thirteen seconds apart, under one handle, through a close
|
|
2446
|
+
and a re-offer. Everything that names a task after the fact -- a dial outcome, a media event, an
|
|
2447
|
+
ending, a history report, a command -- would land on whichever assignment is open when it arrives.
|
|
2448
|
+
So the assignment id is unique within the provider and never reused: where the platform's handle
|
|
2449
|
+
never comes back, the adapter passes it through; where it does, the adapter mints the assignment id
|
|
2450
|
+
and keeps the mapping, and the desk never learns which. A late event finds the assignment it
|
|
2451
|
+
belongs to or is refused, and a task-scoped browser session (`PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME`)
|
|
2452
|
+
lives one assignment, never the next customer's cookies. A dial the agent application placed was
|
|
2453
|
+
already safe, since its outcome is placed by the agent application's own `dialId`; where the
|
|
2454
|
+
assignment earns its place is everything the agent application does not mint --
|
|
2461
2455
|
`task-media-started`, `task-media-ended`, `task-ended`, a `recordStep` naming a task -- any of which
|
|
2462
|
-
would otherwise find the
|
|
2463
|
-
the adapter keeps, not a field held in memory beside it:
|
|
2464
|
-
|
|
2465
|
-
|
|
2456
|
+
would otherwise find the next customer under a reused handle and act on them. A minted id is part
|
|
2457
|
+
of the task the adapter keeps, not a field held in memory beside it: it lives in the login's
|
|
2458
|
+
`store` with the task, or is derived from a platform handle that itself never reuses, so a rebuilt
|
|
2459
|
+
adapter comes back with the same assignment on the same task, or every late event it was minted to
|
|
2460
|
+
catch mismatches and the second adapter's snapshot no longer carries the task (`drive.reload.snapshot`).
|
|
2466
2461
|
|
|
2467
|
-
|
|
2462
|
+
Assignment, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
|
|
2468
2463
|
task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
|
|
2469
2464
|
subsequent transitions to `preview` or `in-progress`; Omni does not infer them from the acceptance
|
|
2470
2465
|
command.
|
|
@@ -2530,10 +2525,10 @@ it cannot safely auto-trigger and must expose the uncertainty and reconcile. No
|
|
|
2530
2525
|
receipt-time or local-default deadline is inferred. Neither elapsed time nor submission changes
|
|
2531
2526
|
the task phase: subsequent authoritative events state ringing, answer, media and completion.
|
|
2532
2527
|
|
|
2533
|
-
An agent application must serialize manual clicks and its timer under the exact provider/login/task/
|
|
2528
|
+
An agent application must serialize manual clicks and its timer under the exact provider/login/task/assignment
|
|
2534
2529
|
scope, allowing at most one unresolved submission. Recheck the current phase, deadline owner,
|
|
2535
|
-
|
|
2536
|
-
cancels an obsolete timer; login loss, disconnect, task end and
|
|
2530
|
+
assignment and whether an attempt already exists before dispatch. A new snapshot or policy update
|
|
2531
|
+
cancels an obsolete timer; login loss, disconnect, task end and assignment replacement fence its
|
|
2537
2532
|
callback. Reconnect past a deadline first reconciles current task and attempt state; it must not
|
|
2538
2533
|
blindly submit again. An uncertain prior submission remains unresolved, without automatic retry.
|
|
2539
2534
|
The provider atomically arbitrates any residual manual/timer race and does not accept a second
|
|
@@ -2562,14 +2557,13 @@ prerequisites decide which controls are offered. Caller disconnect also executes
|
|
|
2562
2557
|
TaskCommand or task capability. The separately declared agent application recording facility keeps its existing
|
|
2563
2558
|
recording contract; this distinction does not remove it or move provider call operations to the agent application.
|
|
2564
2559
|
|
|
2565
|
-
A caller journey may revisit the same agent while an earlier interaction still wraps.
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
and recheck at the provider; the validator cannot prove source freshness or authorization.
|
|
2560
|
+
A caller journey may revisit the same agent while an earlier interaction still wraps. Each is its
|
|
2561
|
+
own assignment, and two open at once carry two assignment ids. Restore them unchanged on
|
|
2562
|
+
reconnect. Commands, media, endings and history reports name the exact assignment; never retarget
|
|
2563
|
+
an old command to the newest interaction of a journey. `validateTaskCommandRequest` checks the
|
|
2564
|
+
request's assignment against the supplied published task and then its command prerequisites. The
|
|
2565
|
+
caller must select that task within the correct provider/login and recheck at the provider; the
|
|
2566
|
+
validator cannot prove source freshness or authorization.
|
|
2573
2567
|
`task-ended` retires only that interaction's resources. Another interaction, caller channel or IVR/queue
|
|
2574
2568
|
portion must not be cleared because this task ended. Snapshot counts include all still-owned wrap
|
|
2575
2569
|
and active interactions; task completion is not caller hangup.
|
|
@@ -2719,37 +2713,47 @@ const immediateProviderCompletion = {
|
|
|
2719
2713
|
} satisfies Pick<Task, "completionMode" | "wrapAllowance">;
|
|
2720
2714
|
```
|
|
2721
2715
|
|
|
2722
|
-
###
|
|
2716
|
+
### History
|
|
2723
2717
|
|
|
2724
|
-
The
|
|
2725
|
-
`
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2718
|
+
The record is the call's, not the task's: most of what it holds happened before this agent, and
|
|
2719
|
+
`queued` and `offered` are not interactions at all, so the field is `history` and its types are
|
|
2720
|
+
`TaskHistory`, `TaskHistoryStep`, `HistoryStep`, `HistoryReport` and `HistoryReportResult`.
|
|
2721
|
+
*Interaction* keeps the one meaning **Terms** gives it, the agent's time on the call, which is why
|
|
2722
|
+
the total of earlier agents' time is `interactionSeconds` and the phase rules are
|
|
2723
|
+
`command.phase.interaction`. Hosts and providers adopt these names together; no legacy aliases
|
|
2724
|
+
are provided. The manifest's `completionSettleMs` bounds final task completion, not entry into
|
|
2725
|
+
the `completing` wrap-up phase. The `recordStep` method and `complete` command keep their names.
|
|
2729
2726
|
|
|
2730
|
-
Migration from the earlier
|
|
2727
|
+
Migration from the earlier spellings:
|
|
2731
2728
|
|
|
2732
2729
|
| Former API | Current API |
|
|
2733
2730
|
| --- | --- |
|
|
2734
|
-
| Task.handlingHistory | `Task.
|
|
2735
|
-
| TaskHandlingHistory,
|
|
2736
|
-
|
|
|
2737
|
-
|
|
|
2738
|
-
|
|
|
2731
|
+
| Task.handlingHistory, Task.interactionHistory | `Task.history` |
|
|
2732
|
+
| TaskHandlingHistory, TaskInteractionHistory | `TaskHistory` |
|
|
2733
|
+
| TaskHandlingStep, HandlingStep, TaskInteractionStep, InteractionStep | `TaskHistoryStep`, `HistoryStep` |
|
|
2734
|
+
| HandlingReport, HandlingReportResult, validateHandlingReport, and their Interaction spellings | `HistoryReport`, `HistoryReportResult`, `validateHistoryReport` |
|
|
2735
|
+
| HANDLING_STEPS_WITH_A_PERSON, HANDLING_STEPS_THAT_DIAL, and their INTERACTION spellings | `HISTORY_STEPS_WITH_A_PERSON`, `HISTORY_STEPS_THAT_DIAL` |
|
|
2736
|
+
| handlingStepExpectsAPerson, handlingStepDials, and their interaction spellings | `historyStepExpectsAPerson`, `historyStepDials` |
|
|
2739
2737
|
| handleSeconds | `interactionSeconds` |
|
|
2738
|
+
| Task.allocationId, AllocationId, allocationExpiresAt | `Task.assignmentId`, `AssignmentId`, `assignmentExpiresAt` |
|
|
2739
|
+
| Task.id, TaskId, and `taskId` on every event, command, report and lead request | gone: a task is named by `assignmentId` alone, and `assignmentKey(providerId, assignmentId)` scopes it |
|
|
2740
|
+
| taskKey, omni.task-not-found, PROVIDER_NAME__TASK_ID__TAB_NAME (ProviderName.TaskId.TabName) | `assignmentKey`, `omni.assignment-not-found`, `PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME` (`ProviderName.AssignmentId.TabName`) |
|
|
2740
2741
|
| Manifest.disposalSettleMs | `Manifest.completionSettleMs` |
|
|
2741
2742
|
|
|
2742
2743
|
Update producers, consumers, saved task snapshots, and validation-rule assertions together.
|
|
2743
|
-
History
|
|
2744
|
-
|
|
2745
|
-
|
|
2744
|
+
History and report rule names use `history` and `historyReport`; assignment rules use
|
|
2745
|
+
`assignment` (`task.assignmentId`, `stream.taskOffered.duplicate`, `stream.assignment.ended`);
|
|
2746
|
+
phase and conformance rules use `interaction` and final-task rules use `completion`. The former
|
|
2747
|
+
fields are refused even when the new field is also supplied (`task.history.renamed`,
|
|
2748
|
+
`task.assignmentId.renamed` for a task's `id` or `allocationId`, `event.assignmentId.renamed`,
|
|
2749
|
+
`command.request.assignmentId.renamed` and `historyReport.assignmentId.renamed` for a `taskId`).
|
|
2746
2750
|
|
|
2747
2751
|
|
|
2748
|
-
`Task.
|
|
2752
|
+
`Task.history` is the call record: the steps that brought the task to the agent, oldest
|
|
2749
2753
|
first, and what they add up to before this agent:
|
|
2750
2754
|
|
|
2751
2755
|
```ts
|
|
2752
|
-
|
|
2756
|
+
history: {
|
|
2753
2757
|
steps: [
|
|
2754
2758
|
{ step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
|
|
2755
2759
|
{ step: "answered", at: "2026-08-21T00:59:41Z", seconds: 312, by: "a-17" },
|
|
@@ -2772,7 +2776,7 @@ something else, which will arrive under its own name and must not be folded in h
|
|
|
2772
2776
|
It rides in the snapshot and is replaced whole like everything else there.
|
|
2773
2777
|
|
|
2774
2778
|
Steps are `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conferenced`,
|
|
2775
|
-
`unanswered`, and each is defined on `
|
|
2779
|
+
`unanswered`, and each is defined on `TaskHistoryStep`.
|
|
2776
2780
|
|
|
2777
2781
|
**The record is one entry per occurrence, oldest first.** Each hold is its own `held` entry — `at`
|
|
2778
2782
|
when it began, `seconds` once it ended and omitted while it runs — and a second hold is a second
|
|
@@ -2780,7 +2784,7 @@ entry after the first, never a revision of it. A leg that has ended states its d
|
|
|
2780
2784
|
task says whether a leg can still be running: a hold runs only while the task is `paused`, a mute
|
|
2781
2785
|
only while its media is up, so a `held` entry without `seconds` on a task that is not paused, or a
|
|
2782
2786
|
`muted` one on a call that is over, is a leg nobody closed and reads exactly like a leg running now
|
|
2783
|
-
(`task.
|
|
2787
|
+
(`task.history.held.open`, `.muted.open`). Whoever performs the leg closes it — the
|
|
2784
2788
|
provider whose platform parks the caller, the agent application whose microphone it is — by restating the entry
|
|
2785
2789
|
with its duration, and **an entry that cannot be closed is not written**: open for ever is a
|
|
2786
2790
|
plausible nought one level down from a total. **A leg still open when the media ends is closed by
|
|
@@ -2793,7 +2797,7 @@ provider closed it. One publisher of the closed leg, and no order between them t
|
|
|
2793
2797
|
hold, at the entry's `at` plus its `seconds`, and nothing is lost by not naming it twice. The same goes for every step: two mutes are two
|
|
2794
2798
|
`muted` entries, and a call that joined two queues -- a menu's, then this one -- has two `queued`
|
|
2795
2799
|
entries, one per join. Order is enforced: an entry earlier than the one before it is refused
|
|
2796
|
-
(`task.
|
|
2800
|
+
(`task.history.order`). **Intervals between steps are the agent application's to subtract**, never a
|
|
2797
2801
|
total the provider adds: *time to offer*, from the call's arrival to the agent's screen lighting
|
|
2798
2802
|
up, is the `offered` entry's `at` minus the last `queued` entry's, and the ring is outside it.
|
|
2799
2803
|
`queueSeconds` keeps the industry's meaning -- the whole wait until somebody answered -- and is not
|
|
@@ -2807,7 +2811,7 @@ on a resync snapshot -- carries every entry the agent application has already re
|
|
|
2807
2811
|
may add to them; it never has fewer, and never drops the record while the task is open. A fact
|
|
2808
2812
|
stated to the agent application and then withdrawn without an event is a record contradicting itself, exactly
|
|
2809
2813
|
as a task that lost the terms it had read would be, and the stream refuses it the same way
|
|
2810
|
-
(`stream.taskUpdated.
|
|
2814
|
+
(`stream.taskUpdated.history`, `stream.snapshot.history`). A provider whose
|
|
2811
2815
|
platform cannot hold a leg the agent application reported keeps it in the login's `store` for the life of the
|
|
2812
2816
|
task, so a reload of the agent application restates the same record; the entry is keyed by `step` and `at`, so
|
|
2813
2817
|
a running hold restated with its final `seconds` is the same entry.
|
|
@@ -2842,12 +2846,12 @@ counting the wrong thing.
|
|
|
2842
2846
|
the agent application's** — so without a step the provider, which alone keeps the task's record, would have no
|
|
2843
2847
|
account of a period the agent could not be heard. Each `muted` entry carries `mutedBy`, as the agent application
|
|
2844
2848
|
reported it: `host` for the agent's own Mute, `station` for a headset or system mute the agent application
|
|
2845
|
-
observed. No other step has it (`task.
|
|
2849
|
+
observed. No other step has it (`task.history.mutedBy`, `.mutedBy.unexpected`).
|
|
2846
2850
|
|
|
2847
2851
|
**A step that dialled says which dial and where.** `transferred`, `conferenced` and `unanswered`
|
|
2848
2852
|
each end a dial, so the entry carries the `destinationId` it went to and, where an agent application placed it, the
|
|
2849
2853
|
`dialId` the agent application minted; no other step dialled, and either field on one is refused
|
|
2850
|
-
(`task.
|
|
2854
|
+
(`task.history.dialId.unexpected`). This is how a dial made before a transfer is placeable
|
|
2851
2855
|
by whoever holds the task now: a `dial-outcome` naming a `dialId` the agent application never minted is looked
|
|
2852
2856
|
up in the record, not discarded.
|
|
2853
2857
|
|
|
@@ -2870,10 +2874,10 @@ Four rules a provider has to keep:
|
|
|
2870
2874
|
|
|
2871
2875
|
**An absent `by` means different things on different steps, and both are legitimate.** On
|
|
2872
2876
|
`queued` nobody takes part, so there is nothing to name. On every other step somebody did — see
|
|
2873
|
-
`
|
|
2877
|
+
`HISTORY_STEPS_WITH_A_PERSON` and `historyStepExpectsAPerson()` — so an absent `by` there says
|
|
2874
2878
|
*this was handled and the provider cannot say by whom*. The one step that is never unattributed is
|
|
2875
2879
|
`muted`: the agent application has exactly one agent and the provider knows who, so `by` is required there
|
|
2876
|
-
(`task.
|
|
2880
|
+
(`task.history.muted.by`).
|
|
2877
2881
|
|
|
2878
2882
|
That case is ordinary rather than theoretical: a leg answered on a shared phone, a manager's
|
|
2879
2883
|
handset, or a device the provider cannot resolve to a person. **Report the step without `by`
|
|
@@ -2885,7 +2889,7 @@ An agent application must render the two differently. Showing an unattributed `a
|
|
|
2885
2889
|
`queued` tells the agent nobody was involved, which is not what was said. Omni renders it as
|
|
2886
2890
|
*"not recorded"* in the place the name would go.
|
|
2887
2891
|
|
|
2888
|
-
Omit `
|
|
2892
|
+
Omit `history` entirely when the provider cannot observe the steps. Empty `steps` is a
|
|
2889
2893
|
different claim — it says the task has had none.
|
|
2890
2894
|
|
|
2891
2895
|
**What the record adds up to is stated, not summed.** A call that has changed hands arrives
|
|
@@ -2906,13 +2910,12 @@ writes it into its record as it writes every other leg:
|
|
|
2906
2910
|
|
|
2907
2911
|
```ts
|
|
2908
2912
|
declare const connection: Connection<"voice">;
|
|
2909
|
-
declare const
|
|
2910
|
-
declare const allocationId: AllocationId;
|
|
2913
|
+
declare const assignmentId: AssignmentId;
|
|
2911
2914
|
declare const at: IsoTimestamp;
|
|
2912
2915
|
|
|
2913
|
-
void connection.recordStep?.({
|
|
2914
|
-
void connection.recordStep?.({
|
|
2915
|
-
void connection.recordStep?.({
|
|
2916
|
+
void connection.recordStep?.({ assignmentId, step: "muted", at, mutedBy: "host" }); // the moment the agent mutes
|
|
2917
|
+
void connection.recordStep?.({ assignmentId, step: "muted", at, mutedBy: "host", seconds: 15 }); // still running, if the agent application chooses to say
|
|
2918
|
+
void connection.recordStep?.({ assignmentId, step: "muted", at, mutedBy: "host", seconds: 42, ended: true }); // the moment they unmute
|
|
2916
2919
|
```
|
|
2917
2920
|
|
|
2918
2921
|
Every report about one leg repeats its original `step` and agent application `at` as a correlation key;
|
|
@@ -2922,13 +2925,13 @@ elapsed while the leg runs and final once it has ended — and **the end is stat
|
|
|
2922
2925
|
inferred**: `ended: true` marks the last report, and it carries the final duration. **What a
|
|
2923
2926
|
provider never asked for never crosses.** The running report is sent only to a provider whose
|
|
2924
2927
|
manifest declares `runningStepReports`; every other provider receives exactly two reports per
|
|
2925
|
-
leg, when it began and when it ended, and `
|
|
2926
|
-
a running one it was never asked for (`
|
|
2928
|
+
leg, when it began and when it ended, and `validateHistoryReport(report, path, manifest)` refuses
|
|
2929
|
+
a running one it was never asked for (`historyReport.running.unexpected`). What a provider that
|
|
2927
2930
|
did ask for them forwards upstream, and how often, is its own business. The step appears in
|
|
2928
|
-
`
|
|
2931
|
+
`history` when the *provider* publishes it: Omni never writes the record itself.
|
|
2929
2932
|
`recordStep` is required of every softphone login's connection, since every call on a softphone
|
|
2930
2933
|
can be muted by the agent application, and answers `recorded` with the canonical history `at`. A `muted` report says whose the silence was,
|
|
2931
|
-
`mutedBy: "host"` or `"station"`, and no other report has the word (`
|
|
2934
|
+
`mutedBy: "host"` or `"station"`, and no other report has the word (`historyReport.mutedBy`,
|
|
2932
2935
|
`.mutedBy.unexpected`). On a desk phone the microphone is the phone's:
|
|
2933
2936
|
the agent application mutes nothing and records nothing.
|
|
2934
2937
|
|
|
@@ -2951,7 +2954,7 @@ At a provider-confirmed task/media end, the agent application also stops the ass
|
|
|
2951
2954
|
its observed ending where the report is still accepted. An agent application closing report repeats its original
|
|
2952
2955
|
key and may include its measured duration, but cannot reverse an already confirmed provider end.
|
|
2953
2956
|
The provider acknowledges a known closed leg without rewriting its final record; after the task
|
|
2954
|
-
has been completed, it may refuse the report as
|
|
2957
|
+
has been completed, it may refuse the report as assignment-not-found. The agent application reads that response rather
|
|
2955
2958
|
than retrying or recreating the task. An observed agent application end before a provider closure is still
|
|
2956
2959
|
reported normally; the provider decides the final timestamp and publishes the record.
|
|
2957
2960
|
|
|
@@ -3047,7 +3050,7 @@ The supported `BrowserIsolationScheme` values, declared under **Shapes**, key as
|
|
|
3047
3050
|
|
|
3048
3051
|
| Enum member | Example session key |
|
|
3049
3052
|
| --- | --- |
|
|
3050
|
-
| `
|
|
3053
|
+
| `PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME` | `mailflow.EMAIL-829102%2Ea1.CRM` -- the task's assignment id, so one assignment, never the next customer's cookies |
|
|
3051
3054
|
| `TAB_NAME` | `CRM` |
|
|
3052
3055
|
| `PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME` | `mailflow.Support.CRM` |
|
|
3053
3056
|
| `PROVIDER_NAME__TAB_NAME` | `mailflow.CRM` |
|
|
@@ -3131,7 +3134,7 @@ Terms once read stay read: a re-read that fails mid-task is not a new fact about
|
|
|
3131
3134
|
last statement stands and the failure is a `diagnostic`, and a task that was published under
|
|
3132
3135
|
`queue` or `ungoverned` never returns to `undetermined`, on an update (`stream.taskUpdated.capabilitySource`)
|
|
3133
3136
|
or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
|
|
3134
|
-
entry the same two ways (`stream.taskUpdated.
|
|
3137
|
+
entry the same two ways (`stream.taskUpdated.history`, `stream.snapshot.history`);
|
|
3135
3138
|
a snapshot carrying a task still at work does not forget the audio the stream held up, since media
|
|
3136
3139
|
ends on `task-media-ended` and the call moves on (`stream.snapshot.media`); a voice task ends after
|
|
3137
3140
|
its audio ends, never around it, whatever the outcome (`stream.taskEnded.mediaOpen`); and a task does not go
|
|
@@ -3301,7 +3304,7 @@ provider.
|
|
|
3301
3304
|
The agent application sends the ordinary task-scoped `execute` request with `command: { type: "end-call" }`.
|
|
3302
3305
|
The provider knows the caller and agent-added channel bindings; no channel list or owner details
|
|
3303
3306
|
are sent by the agent application. The task's `endCall` capability is permission, not an executor selection.
|
|
3304
|
-
`validateTaskCommandRequest` checks the exact interaction/
|
|
3307
|
+
`validateTaskCommandRequest` checks the exact interaction/assignment and delegates the command's
|
|
3305
3308
|
policy/phase checks. The provider rechecks current ownership atomically; no client validator can
|
|
3306
3309
|
prove that its view is fresh. The agent application does not implement agent end-call; low-level removal of individual channels is not
|
|
3307
3310
|
this agent action. There is no agent application end-call execution API or fallback route.
|
|
@@ -3310,7 +3313,7 @@ this agent action. There is no agent application end-call execution API or fallb
|
|
|
3310
3313
|
all agent-added channels owned by that interaction, including channels originally added by previous
|
|
3311
3314
|
agents and inherited through transfer or takeover. Ownership is not inferred from who originally
|
|
3312
3315
|
dialed, the current UI selection, or a person's label. The provider maintains that authority
|
|
3313
|
-
and checks the exact interaction/
|
|
3316
|
+
and checks the exact interaction/assignment when acting. Agent-added channels are not left behind
|
|
3314
3317
|
merely because another agent added them.
|
|
3315
3318
|
|
|
3316
3319
|
On a successful transfer or takeover, ownership of those channels passes to the receiving
|
|
@@ -3425,7 +3428,7 @@ control and invokes it with the shared custom task command:
|
|
|
3425
3428
|
}
|
|
3426
3429
|
```
|
|
3427
3430
|
|
|
3428
|
-
`name` is the `id` of the custom capability the agent used. There is no `
|
|
3431
|
+
`name` is the `id` of the custom capability the agent used. There is no `assignmentId` here: like every
|
|
3429
3432
|
other command it travels on the `TaskCommandRequest` around it. A `toggle` carries the state it
|
|
3430
3433
|
wants — `{ type: "custom", name: "mark-vip", on: true }` — never a flip, for the reason under
|
|
3431
3434
|
**Task commands**.
|
|
@@ -3464,26 +3467,25 @@ across the installation, and it travels four ways so no leg is a lookup:
|
|
|
3464
3467
|
|
|
3465
3468
|
```ts
|
|
3466
3469
|
declare const connection: Connection<"voice">;
|
|
3467
|
-
declare const
|
|
3468
|
-
declare const allocationId: AllocationId;
|
|
3470
|
+
declare const assignmentId: AssignmentId;
|
|
3469
3471
|
|
|
3470
3472
|
// 1. Out, minted by the agent application.
|
|
3471
|
-
const result = await connection.execute({
|
|
3473
|
+
const result = await connection.execute({ assignmentId, command: { type: "conference", action: "add", dialId: "dial-7f2", destinationId: "tier2" } });
|
|
3472
3474
|
|
|
3473
3475
|
// 2. Back on the result, restated, so the agent application compares and confirms.
|
|
3474
3476
|
expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
|
|
3475
3477
|
|
|
3476
3478
|
// 3. On the outcome, however late, against the task it belonged to.
|
|
3477
|
-
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2",
|
|
3479
|
+
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", assignmentId, destinationId: "tier2", outcome: "no-answer", reason: "No route to destination" };
|
|
3478
3480
|
|
|
3479
3481
|
// 4. In the record, so a dial made before a transfer is placeable by whoever holds the task now.
|
|
3480
|
-
const step:
|
|
3482
|
+
const step: TaskHistoryStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destinationId: "tier2" };
|
|
3481
3483
|
```
|
|
3482
3484
|
|
|
3483
3485
|
The identity is a correlation handle between agent application and provider and **is never shown to the
|
|
3484
3486
|
agent**. An agent application holding several tasks places an outcome by the `dialId` it minted; one it did not
|
|
3485
3487
|
mint -- a dial made by the previous agent before a transfer -- it finds on the task's `onCall` or in
|
|
3486
|
-
its `
|
|
3488
|
+
its `history`, which is why the steps that dial carry it. The `dialId` is not a retry key:
|
|
3487
3489
|
Omni never repeats a dial, and a command still carries no key for that purpose.
|
|
3488
3490
|
|
|
3489
3491
|
**`dialling` is the answer to a dial, and `applied` is not.** A command that dials answers
|
|
@@ -3506,14 +3508,14 @@ create dialog" in words and still name no cause a code would carry, so `unexplai
|
|
|
3506
3508
|
`reason` is a dial the switch described but did not classify, and a desk renders the words without
|
|
3507
3509
|
inventing the class.
|
|
3508
3510
|
|
|
3509
|
-
**`
|
|
3511
|
+
**`assignmentId` is the task the dial was placed on**, resolved from the dial's own identity and never from
|
|
3510
3512
|
whatever task the agent is looking at when the outcome arrives. That task may already have ended,
|
|
3511
3513
|
and the harness places the outcome all the same (`TaskStream`, `stream.dialOutcome.unknown`,
|
|
3512
3514
|
`stream.dialOutcome.duplicate`); an agent who moved on still learns nobody was reached, against the
|
|
3513
3515
|
call it belonged to and never against the next one.
|
|
3514
3516
|
|
|
3515
3517
|
The stream knows a dial from three places: `TaskStream.dialled(dialId)`, which an agent application calls when it
|
|
3516
|
-
places one; an entry on a task's `onCall`; and a step in its `
|
|
3518
|
+
places one; an entry on a task's `onCall`; and a step in its `history`. Since a dialled entry
|
|
3517
3519
|
is listed from placement, `dialled()` is load-bearing only for a dial that never has an entry -- a
|
|
3518
3520
|
cold transfer, a connect-back, an idle dialpad call -- or whose entry has already left the room; an
|
|
3519
3521
|
outcome for a dial nobody placed and no task mentions is what `stream.dialOutcome.unknown` refuses.
|
|
@@ -3789,14 +3791,14 @@ hold applied. A `held` result would repeat the discriminant that travelled with
|
|
|
3789
3791
|
|
|
3790
3792
|
### `setCapacity(capacity)`
|
|
3791
3793
|
|
|
3792
|
-
States how many tasks this provider may have
|
|
3794
|
+
States how many tasks this provider may have assigned to the agent **at once**.
|
|
3793
3795
|
|
|
3794
3796
|
`count` is an absolute ceiling, not an increment, a whole number of zero or more
|
|
3795
3797
|
(`capacity.count`). An agent's capacity is a property of the agent, not of the moment: it is
|
|
3796
3798
|
stated when the agent is set up and restated only when it genuinely changes, which is a
|
|
3797
3799
|
local policy change rather than a task starting or ending -- with one exception below.
|
|
3798
3800
|
|
|
3799
|
-
**The provider counts its own outstanding tasks against it.**
|
|
3801
|
+
**The provider counts its own outstanding tasks against it.** Assign while you hold fewer than
|
|
3800
3802
|
`count` tasks for this agent, and stop when you hold that many; when one of yours ends you have
|
|
3801
3803
|
room again and need no new signal to know it. Omni does not re-state capacity as tasks come and
|
|
3802
3804
|
go, and a provider that waits for it will stall.
|
|
@@ -3804,7 +3806,7 @@ go, and a provider that waits for it will stall.
|
|
|
3804
3806
|
Your own tasks are the only ones you count. What the agent holds at other providers is not your
|
|
3805
3807
|
concern — Omni set `count` knowing it, and this is how: the agent is one person on several
|
|
3806
3808
|
providers, and the agent application divides their capacity among them rather than telling each the whole. A
|
|
3807
|
-
provider that has none of it for now is told **`count: 0`, agent application-stopped**:
|
|
3809
|
+
provider that has none of it for now is told **`count: 0`, agent application-stopped**: assign nothing, show
|
|
3808
3810
|
the member as `reserved` on the team member list -- signed in here, capacity held by the agent application for elsewhere,
|
|
3809
3811
|
a fact this provider holds, where `on-task` would assert work it cannot see -- and take the next
|
|
3810
3812
|
count as any other when the agent application has capacity for this provider again. Zero is the one restatement
|
|
@@ -3812,7 +3814,7 @@ that follows work rather than local policy.
|
|
|
3812
3814
|
|
|
3813
3815
|
**A capacity is taken, never refused.** `setCapacity` answers `applied` and nothing else: a
|
|
3814
3816
|
statement has no failure to report, and a `failed` arm gave four agent application-provider pairings four
|
|
3815
|
-
readings of which ceiling stood after it. A provider that cannot carry the count
|
|
3817
|
+
readings of which ceiling stood after it. A provider that cannot carry the count assigns within
|
|
3816
3818
|
what it can and says so on a `diagnostic`, so the gap is visible and nothing is inferred from a
|
|
3817
3819
|
refusal.
|
|
3818
3820
|
|
|
@@ -3823,9 +3825,9 @@ taking capacity away is answered `applied` while the work keeps coming. The harn
|
|
|
3823
3825
|
both ways after the drive, two then one then nought, and an offer after a lower count is caught
|
|
3824
3826
|
against it (`stream.taskOffered.overCapacity`).
|
|
3825
3827
|
|
|
3826
|
-
**Capacity gates what the provider
|
|
3828
|
+
**Capacity gates what the provider assigns, not what the agent starts.** A call placed from the
|
|
3827
3829
|
idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
|
|
3828
|
-
it: the ceiling binds
|
|
3830
|
+
it: the ceiling binds assignment, not the agent's own hand.
|
|
3829
3831
|
|
|
3830
3832
|
### Runtime break prerequisite checks
|
|
3831
3833
|
|
|
@@ -4214,23 +4216,24 @@ Required when the login declares `capabilities.team.leadAssistControl`, and gate
|
|
|
4214
4216
|
|
|
4215
4217
|
```ts
|
|
4216
4218
|
// 1. The agent asks, with a small note. Their task carries `leadAssist` from here on.
|
|
4217
|
-
execute({
|
|
4219
|
+
execute({ assignmentId: "alloc-42", command: { type: "lead-assist", action: "request", note: "Refund dispute, needs approval" } })
|
|
4218
4220
|
// task.leadAssist = { stage: "requested", note: "Refund dispute, needs approval", since }
|
|
4219
4221
|
|
|
4220
4222
|
// 2. Every lead entitled to it sees the request on their team member list.
|
|
4221
|
-
// team-updated: requests: [{ id: "req-7", memberId: "A-1",
|
|
4223
|
+
// team-updated: requests: [{ id: "req-7", memberId: "A-1", assignmentId: "alloc-42", note, since }]
|
|
4222
4224
|
|
|
4223
4225
|
// 3. A lead joins, or declines.
|
|
4224
4226
|
executeTeamLeadAssist({ command: { type: "join", requestId: "req-7" } })
|
|
4225
4227
|
executeTeamLeadAssist({ command: { type: "decline", requestId: "req-7", reason: "In a call" } })
|
|
4226
4228
|
```
|
|
4227
4229
|
|
|
4228
|
-
**On `join` the provider bridges three parties and the lead is on a task of their own**,
|
|
4229
|
-
|
|
4230
|
-
-- the way a call an agent placed themselves arrives -- and carrying `assisting
|
|
4230
|
+
**On `join` the provider bridges three parties and the lead is on a task of their own**, an
|
|
4231
|
+
assignment of its own, arriving on the lead's connection as `task-offered` with `automatic`
|
|
4232
|
+
-- the way a call an agent placed themselves arrives -- and carrying `assisting`, which names the
|
|
4233
|
+
member and the member's assignment. The agent's task
|
|
4231
4234
|
moves to `leadAssist: { stage: "joined", leadId }`. A join is the lead's own act, so capacity does not
|
|
4232
4235
|
trigger it; but from then on it is an outstanding task the provider counts against the lead's
|
|
4233
|
-
stated ceiling like any other, nothing more is
|
|
4236
|
+
stated ceiling like any other, nothing more is assigned to the lead while it stands, and a
|
|
4234
4237
|
provider whose lead is already at the ceiling answers the join `failed`.
|
|
4235
4238
|
|
|
4236
4239
|
**A lead assists one call at a time.** A `join` from a lead already on a call -- their own or one
|
|
@@ -4338,8 +4341,8 @@ declaration requires `executeTeamListen`, gated exactly as `executeTeamLeadAssis
|
|
|
4338
4341
|
executeTeamListen({ command: { type: "listen", memberId: "A-1" } })
|
|
4339
4342
|
|
|
4340
4343
|
// 2. The lead's own task arrives -- task-offered with `automatic`, as a joined call does -- and
|
|
4341
|
-
// carries `listening`; the
|
|
4342
|
-
// listening: { memberId: "A-1",
|
|
4344
|
+
// carries `listening`; the assignment is the lead's own, the member's is named inside it.
|
|
4345
|
+
// listening: { memberId: "A-1", assignmentId: "alloc-42", mode: "listen", since }
|
|
4343
4346
|
|
|
4344
4347
|
// 3. The lead changes how they are heard; the provider restates the task with the new mode.
|
|
4345
4348
|
executeTeamListen({ command: { type: "coach" } })
|
|
@@ -4377,7 +4380,7 @@ const listeningLead = {
|
|
|
4377
4380
|
channel: "voice",
|
|
4378
4381
|
capabilities: {},
|
|
4379
4382
|
phase: "in-progress",
|
|
4380
|
-
listening: { memberId: "A-1",
|
|
4383
|
+
listening: { memberId: "A-1", assignmentId: "alloc-42", mode: "coach", since: "2026-08-21T09:04:00Z" },
|
|
4381
4384
|
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "listening">;
|
|
4382
4385
|
```
|
|
4383
4386
|
|
|
@@ -4430,7 +4433,7 @@ mode stays with the login rather than being inferred from a snapshot or a device
|
|
|
4430
4433
|
Nor does the audio ever stand in for the task: a task's presence and
|
|
4431
4434
|
phase follow the provider's reports about the work, and the media — attaching, moving through a
|
|
4432
4435
|
hold, a consult, a conference or a transfer, and ending — is transient beside it. See **A task is
|
|
4433
|
-
never its audio** under **Task
|
|
4436
|
+
never its audio** under **Task assignment lifecycle**.
|
|
4434
4437
|
|
|
4435
4438
|
### The agent application reports, the adapter decides
|
|
4436
4439
|
|
|
@@ -4490,7 +4493,7 @@ as reachability opens a window where it believes the agent is available and the
|
|
|
4490
4493
|
set up — the adapter's own registration incomplete, its credentials not yet renewed.
|
|
4491
4494
|
|
|
4492
4495
|
Nothing closes that window, because nothing opens it: **Omni states no capacity until the
|
|
4493
|
-
connection is established**, and **Work is pulled, never pushed** makes an
|
|
4496
|
+
connection is established**, and **Work is pulled, never pushed** makes an assignment with none
|
|
4494
4497
|
stated a violation. Capacity does not wait on the microphone: whether an agent without agent application audio
|
|
4495
4498
|
can take calls is the platform's question, answered by the adapter from the agent application's report, not by
|
|
4496
4499
|
Omni withholding capacity for every platform alike.
|
|
@@ -4500,12 +4503,12 @@ anything to become available.
|
|
|
4500
4503
|
|
|
4501
4504
|
| Situation | What Omni sends |
|
|
4502
4505
|
| --- | --- |
|
|
4503
|
-
| Connecting | Nothing. No capacity has been stated, so nothing may be
|
|
4506
|
+
| Connecting | Nothing. No capacity has been stated, so nothing may be assigned. |
|
|
4504
4507
|
| Connected and idle | `setCapacity({ count: n })`, with the agent application's report already available to consult. |
|
|
4505
4508
|
| A task starts or ends | Nothing. The provider counts its own against the ceiling. |
|
|
4506
4509
|
| The agent's provisioned capacity changes | `setCapacity({ count: n })` |
|
|
4507
4510
|
| Agent asks for a break | `requestBreak`. Capacity is unchanged and work continues. |
|
|
4508
|
-
| Omni commits a break | `commitBreak`. The break stops
|
|
4511
|
+
| Omni commits a break | `commitBreak`. The break stops assignment, not the ceiling. |
|
|
4509
4512
|
| Agent returns from break | `endBreak` |
|
|
4510
4513
|
|
|
4511
4514
|
**A break is not a capacity of zero, and neither is the reverse.** Capacity says how much of this
|
|
@@ -4579,7 +4582,7 @@ is the administrator's, and a login is not a request to reconfigure an agent.
|
|
|
4579
4582
|
`softphone` implements it, because on a softphone the call's audio lands in Omni:
|
|
4580
4583
|
|
|
4581
4584
|
```ts
|
|
4582
|
-
openMedia({
|
|
4585
|
+
openMedia({ assignmentId, localAudio }): Promise<OpenMediaResult>
|
|
4583
4586
|
// { status: "opened", session } | { status: "unavailable", failure }
|
|
4584
4587
|
```
|
|
4585
4588
|
|
|
@@ -4619,7 +4622,7 @@ Command names follow the channel's operational vocabulary, and each channel's co
|
|
|
4619
4622
|
discriminated by `type` — the same discriminant `executeTeamBreak` and `custom` already use. The
|
|
4620
4623
|
unions are declared under **Shapes**.
|
|
4621
4624
|
|
|
4622
|
-
`
|
|
4625
|
+
`assignmentId` is not repeated on the command. It travels on the `TaskCommandRequest` around it.
|
|
4623
4626
|
|
|
4624
4627
|
**A toggle carries the state it wants, not a flip.** Inverting whatever is found cannot converge
|
|
4625
4628
|
with a stale view: a flip against a state the provider has already changed turns something on and
|
|
@@ -4681,10 +4684,10 @@ the endpoint and can clear an operating-system mute. A hardware slider is nobody
|
|
|
4681
4684
|
heard, and the record exists so that period is not a hole. The agent application reports every such period
|
|
4682
4685
|
through `recordStep` -- its own Mute, and a station mute it observed during a call -- with
|
|
4683
4686
|
`mutedBy` saying whose the silence was, and the provider writes the word into the entry
|
|
4684
|
-
(`task.
|
|
4687
|
+
(`task.history.mutedBy`). The report names no agent because the agent application has exactly one, and
|
|
4685
4688
|
the provider knows who that is: it attributes the leg to the login's agent in `by`, a fact it
|
|
4686
4689
|
holds and not an inference, for a station mute as much as for the agent application's own. An unattributed
|
|
4687
|
-
`muted` entry is refused (`task.
|
|
4690
|
+
`muted` entry is refused (`task.history.muted.by`); a shared handset's unattributed hold
|
|
4688
4691
|
is a different claim, and stands. A supervisor reading the record then sees "the agent muted for
|
|
4689
4692
|
forty seconds" and "the agent's headset was muted for forty seconds" as the different things they
|
|
4690
4693
|
are. See **The agent application records what it performs**.
|
|
@@ -4810,7 +4813,7 @@ react rather than only display the message:
|
|
|
4810
4813
|
| --- | --- |
|
|
4811
4814
|
| `omni.not-authenticated` | The provider session is no longer usable. The adapter has published `expired` at or before this answer — the state is what Omni surfaces reauthentication from; the code says why this action failed, and is never the only signal. |
|
|
4812
4815
|
| `omni.capability-not-enabled` | The action targets a capability this task, manifest, or login did not declare — including a lead command from a login whose `capabilities` no longer carry it. |
|
|
4813
|
-
| `omni.
|
|
4816
|
+
| `omni.assignment-not-found` | The assignment named is not one the provider holds, typically after the task already ended. |
|
|
4814
4817
|
| `omni.phone-not-permitted` | The agent application declared a `phone` the platform does not permit for this agent -- a softphone for an agent configured for a desk phone, or the reverse. The login is refused at authentication, and the provider never reconfigures the agent to make the declaration true. See **How the agent hears the call**. |
|
|
4815
4818
|
| `omni.destination-not-permitted` | The dialled number, or the `destinationId` named, is not one the provider offers this agent. |
|
|
4816
4819
|
| `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
|
|
@@ -4848,7 +4851,7 @@ The current exceptions and their reasons are:
|
|
|
4848
4851
|
|
|
4849
4852
|
| Case | Exception and reason |
|
|
4850
4853
|
| --- | --- |
|
|
4851
|
-
| Agent application display of provider deadlines (`
|
|
4854
|
+
| Agent application display of provider deadlines (`assignmentExpiresAt`, `previewEndsAt`, wrap deadline) | A countdown may use an explicitly bounded provider-clock estimate when a trusted direct clock is unavailable, because the deadline belongs to another clock domain. This estimates the display only; it does not establish that the provider acted. Without usable time, show timing uncertainty. |
|
|
4852
4855
|
| Agent application-triggered preview deadline | A bounded clock estimate with monotonic aging may schedule the agent application's Call command because the provider owns the deadline but the agent application owns the trigger. Do not trigger before the deadline is known to have passed; invalidate on clock discontinuity and reconcile when uncertain. |
|
|
4853
4856
|
| Recording evidence expiry | A bounded observer-domain clock estimate with monotonic aging may assess freshness because observation and expiry belong to the recorder's clock. If time cannot be trusted, recording state is unknown; never renew evidence from receipt or replay. |
|
|
4854
4857
|
| Unknown recording state | `observedAt` and `validUntil` are absent because there is no confirmed observation. The containing provider event still has its own `occurredAt`; that publication is not a recorder observation. |
|
|
@@ -5012,7 +5015,7 @@ availability on `BreakState`. Nor does it carry authentication — a session tha
|
|
|
5012
5015
|
the transport underneath may be perfectly `active`. Provider login identity likewise belongs to
|
|
5013
5016
|
authentication state, not here.
|
|
5014
5017
|
|
|
5015
|
-
**Only `active` means work can arrive.** Omni stops expecting
|
|
5018
|
+
**Only `active` means work can arrive.** Omni stops expecting assignments in any other value, so an
|
|
5016
5019
|
adapter that leaves a stale `active` in place is telling Omni to keep waiting for work that cannot
|
|
5017
5020
|
come — see **Liveness**.
|
|
5018
5021
|
|
|
@@ -5150,14 +5153,14 @@ task, since the ending was owed and lost. The drive holds a provider to the same
|
|
|
5150
5153
|
(`drive.completion.unsettled`). The `task-media-ended` event and the `completing` phase are likewise
|
|
5151
5154
|
non-terminal. A replacement
|
|
5152
5155
|
snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
|
|
5153
|
-
same envelope ID is harmless, and a `task-ended` naming an
|
|
5154
|
-
recognised as the late event it is, never applied to
|
|
5156
|
+
same envelope ID is harmless, and a `task-ended` naming an assignment that has already ended is
|
|
5157
|
+
recognised as the late event it is, never applied to whatever assignment is open now.
|
|
5155
5158
|
|
|
5156
5159
|
### `dial-outcome`
|
|
5157
5160
|
|
|
5158
5161
|
How a dial the agent application placed ended, once, either way, named by the `dialId` the agent application sent: `answered`,
|
|
5159
5162
|
or one of the declared ways of not reaching the destination, with the switch's `reason` where it
|
|
5160
|
-
gave one. `
|
|
5163
|
+
gave one. `assignmentId` names the task the dial was on, where there was one, and that task may already
|
|
5161
5164
|
have ended. Omni shows it to the agent against the call it belonged to and does nothing else: no
|
|
5162
5165
|
audio moves, since the provider bridges the leg on its side, and no dial is repeated. See **Every
|
|
5163
5166
|
dial has an outcome**.
|
|
@@ -5173,7 +5176,7 @@ explicit timezones.
|
|
|
5173
5176
|
The provider's report that something its platform answered broke a rule the adapter relies on — a
|
|
5174
5177
|
state read that contradicted itself, a `task-ended` naming a task nobody held, a party arriving
|
|
5175
5178
|
without a name. `expected` is the rule as a sentence, `observed` is what came instead, and
|
|
5176
|
-
`
|
|
5179
|
+
`assignmentId` names the task where there is one. **Informational, never behavioural**: the provider has
|
|
5177
5180
|
already done the safe thing before it speaks, and nothing in the task or break machinery reacts to
|
|
5178
5181
|
a diagnostic. It exists so that the loudness reaches a person — an agent application renders each one where the
|
|
5179
5182
|
agent works and keeps a count an operator can read, because a healthy workspace over a shouting
|
|
@@ -5188,7 +5191,7 @@ fails loudly rather than passing with a note. A task published under `capability
|
|
|
5188
5191
|
| --- | --- |
|
|
5189
5192
|
| `expected` | Required. The rule that was broken, as a sentence a person can read. |
|
|
5190
5193
|
| `observed` | Required. What the platform answered instead. |
|
|
5191
|
-
| `
|
|
5194
|
+
| `assignmentId` | Optional. The task concerned, where there is one, named by its assignment. |
|
|
5192
5195
|
|
|
5193
5196
|
### `queue-summary`
|
|
5194
5197
|
|
|
@@ -5222,10 +5225,11 @@ declares the `calendar` idle capability.
|
|
|
5222
5225
|
|
|
5223
5226
|
## Utilities
|
|
5224
5227
|
|
|
5225
|
-
### `
|
|
5228
|
+
### `assignmentKey(providerId, assignmentId)`
|
|
5226
5229
|
|
|
5227
|
-
Returns a collision-safe global task key by encoding and joining the provider
|
|
5228
|
-
Use this key in Omni state;
|
|
5230
|
+
Returns a collision-safe global task key by encoding and joining the provider id and the
|
|
5231
|
+
assignment id. Use this key in Omni state; an assignment id is unique within its provider and
|
|
5232
|
+
never across providers.
|
|
5229
5233
|
|
|
5230
5234
|
### `userKey(providerId, userId)`
|
|
5231
5235
|
|
|
@@ -5233,7 +5237,7 @@ The same treatment for a `UserId`, and needed for the same reason: user identifi
|
|
|
5233
5237
|
issued by each provider independently, so two providers will eventually issue the same string for
|
|
5234
5238
|
different people. Encode and join before storing or comparing.
|
|
5235
5239
|
|
|
5236
|
-
Use it for every `UserId` — `
|
|
5240
|
+
Use it for every `UserId` — `history[].by`, team members, `memberId` on a
|
|
5237
5241
|
lead command, `ForcedBreak.by`. A bare one is only ever compared against another from the **same** provider; anything
|
|
5238
5242
|
wider goes through this key.
|
|
5239
5243
|
|
|
@@ -5262,7 +5266,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
5262
5266
|
| `validateProviderTimeCheckRequest(request)` | Fresh-request shape; the agent application enforces actual uniqueness and outstanding-request lifetime. |
|
|
5263
5267
|
| `validateProviderTimeCheckResult(result, request, loginId)` | ISO timestamp, clock identity, exact request/login correlation; timing and source accuracy remain runtime checks. |
|
|
5264
5268
|
| `validateProviderTimeEstimate(estimate, scope)` | Optional agent application estimate shape and provider/login scope; no accuracy guarantee. |
|
|
5265
|
-
| `
|
|
5269
|
+
| `validateHistoryReport(report, path?, manifest?)` | What the agent application reports of a leg it performed, for an adapter to check before forwarding: a task, a step, when it began, a positive `seconds` where stated, and an explicit `ended` that carries the final duration. Given the manifest, a running report is refused unless it declares `runningStepReports`. |
|
|
5266
5270
|
| `validateHostReport(report)` | The agent application's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever agent application a test hands the adapter; `stillHost(report)` builds one that never changes. |
|
|
5267
5271
|
| `validateHostMute(mute, softphone)` | What the agent application's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the agent application holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
|
|
5268
5272
|
| `validateLoginStore(store)` | The login's store the agent application hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
|
|
@@ -5344,7 +5348,7 @@ latest the session published during the run, which differs only when the adapter
|
|
|
5344
5348
|
A capability granted by a later login requires its methods just as one declared at sign-in does.
|
|
5345
5349
|
|
|
5346
5350
|
`result.notExercised` lists what the run never reached — one subject per family of rules: each
|
|
5347
|
-
optional part of a task (`task.browsers`, `task.
|
|
5351
|
+
optional part of a task (`task.browsers`, `task.history`, `task.leadAssist`, …), the break's
|
|
5348
5352
|
`reasons` and `forced`, the team member list's `members` and `requests`, each declared contribution, and
|
|
5349
5353
|
each event type (`event.task-ended`, …) — and so what a clean `violations` says nothing about.
|
|
5350
5354
|
Nothing there is a violation: an adapter with no team has nothing to exercise. But a fixture with
|
|
@@ -5378,7 +5382,7 @@ gap and not a pass. With the audio open on a softphone,
|
|
|
5378
5382
|
the drive mutes it for one second and reports the leg through `recordStep`, begun and then ended,
|
|
5379
5383
|
expecting each report `recorded` with the provider-selected history `at` (`drive.recordStep.failed`, `.rejected`, `result.recordStep.at`); then it mutes again and
|
|
5380
5384
|
ends the call muted, as agents do, so the leg is open when the media ends, the provider closes it
|
|
5381
|
-
in the completing publication or the open entry is refused (`task.
|
|
5385
|
+
in the completing publication or the open entry is refused (`task.history.muted.open`),
|
|
5382
5386
|
and the drive's closing report after the media ended is expected `recorded` and to change nothing:
|
|
5383
5387
|
a record restated afterwards with that leg's duration altered is named (`drive.recordStep.overwritten`).
|
|
5384
5388
|
Where the provider restates the task's record afterwards, each leg is in it or the hole is named
|
|
@@ -5391,7 +5395,7 @@ connection to the end. A platform that holds the record hands it back, and an ad
|
|
|
5391
5395
|
the record in memory has nothing and is named (`drive.reload.snapshot`, `drive.reload.history`,
|
|
5392
5396
|
`.rejected`). The second adapter's snapshot is taken as any resync is: held to what the stream knew
|
|
5393
5397
|
before it replaces it, so a phase gone backwards, a record that shrank or audio forgotten on a task
|
|
5394
|
-
still at work is named by the stream's own rules (`stream.snapshot.phase`, `.
|
|
5398
|
+
still at work is named by the stream's own rules (`stream.snapshot.phase`, `.history`,
|
|
5395
5399
|
`.media`) and a reload is a place those rules keep working, not one where they stop. The second
|
|
5396
5400
|
adapter is held to what the first was: the same provider
|
|
5397
5401
|
(`drive.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
|
|
@@ -5404,10 +5408,10 @@ open task back to, and nothing about the reload is exempt from any rule. The sec
|
|
|
5404
5408
|
has already put what would rebuild it into the secrets store, from the moment it was handed one,
|
|
5405
5409
|
not only when a flow completes -- the session is the adapter's, the store is the agent application's, and a
|
|
5406
5410
|
agent application reload is exactly when no flow will run. And once the task has ended, the store the drive
|
|
5407
|
-
handed the adapter holds no key naming the task's id, delimited, never as a run of characters inside
|
|
5411
|
+
handed the adapter holds no key naming the task's assignment id, delimited, never as a run of characters inside
|
|
5408
5412
|
another id (`drive.store.retained`); an adapter whose keys never named the task leaves the rule
|
|
5409
5413
|
unevaluated, and the result says so rather than passing it: a task's keys go
|
|
5410
|
-
with the task, or
|
|
5414
|
+
with the task, or whatever comes next inherits them. Outside the
|
|
5411
5415
|
interaction phases with `hold` still declared -- in
|
|
5412
5416
|
`confirmed`, where the provider publishes it, and in `completing` once this agent's interaction has ended -- the
|
|
5413
5417
|
drive sends `hold` past the validator that would hold it back, and expects `failed`: the adapter
|
|
@@ -5526,11 +5530,11 @@ of no effect. It rejects with unknown outcome, reports the failure visibly and p
|
|
|
5526
5530
|
current capture state is actually known. Retention is not a promise of sample-perfect audio.
|
|
5527
5531
|
|
|
5528
5532
|
Provider commands go exclusively to `Connection.execute`; agent application commands go exclusively to
|
|
5529
|
-
`HostRecording.execute`. Both include
|
|
5533
|
+
`HostRecording.execute`. Both include the assignment, request and observation identities; all
|
|
5530
5534
|
non-start commands identify the particular recording. IDs are opaque and scoped by provider login,
|
|
5531
5535
|
task and recorder owner. They are never inferred from filenames or current agent identity. A
|
|
5532
|
-
recording ID survives pause/resume and
|
|
5533
|
-
commands always name the current
|
|
5536
|
+
recording ID survives pause/resume and reassignment only where the same recorder confirms continuity;
|
|
5537
|
+
commands always name the current assignment. A later start gets a different ID. Reconnect does not
|
|
5534
5538
|
create a new recording or fresh evidence. After lost continuity, use unknown until reconciled.
|
|
5535
5539
|
There is no automatic restart, transfer to another recorder, or stop on task hold/disconnect.
|
|
5536
5540
|
Task removal does not prove recording stopped: outstanding agent application recorders remain tracked by the
|
|
@@ -5549,7 +5553,7 @@ Receipt, replay and task publication never extend freshness. Clock discontinuity
|
|
|
5549
5553
|
consumers using the explicit recording-expiry clock-estimation exception must invalidate that
|
|
5550
5554
|
estimate and use monotonic aging so clock rollback cannot
|
|
5551
5555
|
revive expired evidence. The executor compares observation identity and recording identity against
|
|
5552
|
-
its latest state atomically before I/O. An intervening observation or
|
|
5556
|
+
its latest state atomically before I/O. An intervening observation or assignment change refuses the
|
|
5553
5557
|
stale command; it never acts on a replacement recorder. Providers lacking trustworthy current-state
|
|
5554
5558
|
identity or observation time publish unknown and offer no issuable controls.
|
|
5555
5559
|
|