@xema/omni-protocol 0.1.71 → 0.1.72

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/testing.js CHANGED
@@ -1055,7 +1055,7 @@ export class TaskStream {
1055
1055
  case "task-updated":
1056
1056
  if (id === undefined)
1057
1057
  break;
1058
- ruleEvaluated("stream.taskUpdated.unknown", "stream.taskUpdated.capabilitySource", "stream.taskUpdated.phase", "stream.taskUpdated.allocation", "stream.taskUpdated.handlingHistory", "stream.taskMediaEnded.follow", "stream.taskUpdated.media", "stream.taskUpdated.stage");
1058
+ ruleEvaluated("stream.taskUpdated.unknown", "stream.taskUpdated.capabilitySource", "stream.taskUpdated.phase", "stream.taskUpdated.allocation", "stream.taskUpdated.mediaOpen", "stream.taskUpdated.handlingHistory", "stream.taskMediaEnded.follow", "stream.taskUpdated.media", "stream.taskUpdated.stage");
1059
1059
  if (known === undefined) {
1060
1060
  refuse("stream.taskUpdated.unknown", `${at}.task.id`, `${id} was never offered or carried on a snapshot`);
1061
1061
  break;
@@ -1094,6 +1094,15 @@ export class TaskStream {
1094
1094
  refuse("stream.taskUpdated.handlingHistory", `${at}.task.handlingHistory`, `${id}'s record lost ${lost.join(", ")} on the update: an entry read by the host stays in the record until the task ends`);
1095
1095
  }
1096
1096
  }
1097
+ // A task completes after its media ends, never around it: an update that moves a task to
1098
+ // completing while the stream holds its audio as started is a call whose audio never ended,
1099
+ // whoever caused the ending -- the drive's end-call, a transfer, the provider's own hand.
1100
+ {
1101
+ const to = isRecord(event.task) ? String(event.task.phase) : "";
1102
+ if (to === "completing" && known.media === "started" && known.phase !== "completing") {
1103
+ refuse("stream.taskUpdated.mediaOpen", `${at}.task.phase`, `${id} moves to completing with its media still started: the audio ends first, on task-media-ended, and a wrap-up with the call still up is audio that never ended`);
1104
+ }
1105
+ }
1097
1106
  if (known.media === "ended") {
1098
1107
  const phase = isRecord(event.task) ? String(event.task.phase) : "";
1099
1108
  if (phase !== "completing") {
@@ -1519,11 +1528,18 @@ async function driveOneCall(drive) {
1519
1528
  // 5. End the call, where the agent may.
1520
1529
  if (drive.channel === "voice" && latestTask().phase === "in-progress" && offers("endCall")) {
1521
1530
  if (await send({ type: "end-call" }) !== undefined) {
1531
+ // A task that completes with its audio still up is refused by the stream as it passes
1532
+ // (stream.taskUpdated.mediaOpen); the drive does not also wait out the clock for an ending
1533
+ // that is not coming, so the run reads by the rule and not by its timeout.
1522
1534
  const mediaEnded = await waitFor("task-media-ended after end-call", envelope => {
1523
1535
  const event = envelope.event;
1524
- return event.type === "task-media-ended" && event.taskId === taskId ? event : undefined;
1536
+ if (event.type === "task-media-ended" && event.taskId === taskId)
1537
+ return "ended";
1538
+ if (event.type === "task-updated" && isTask(event.task) && event.task.id === taskId && event.task.phase === "completing")
1539
+ return "completing";
1540
+ return undefined;
1525
1541
  }, cursor);
1526
- if (mediaEnded !== undefined)
1542
+ if (mediaEnded?.found === "ended")
1527
1543
  cursor = mediaEnded.at;
1528
1544
  if (await updated(t => t.phase === "completing", "the task completing after its media ended") !== undefined && offers("hold")) {
1529
1545
  await holdRefusedOutsideHandling();
@@ -1021,6 +1021,12 @@ function validateTaskInto(task, context, path, into) {
1021
1021
  // The room is who is on the call now, and a task outlives its call by the whole of wrap-up: a
1022
1022
  // task whose call has ended -- completing, or media ended -- carries nobody, or the last thing the
1023
1023
  // wire said about the call stays true for ever. Empty and absent both say nobody.
1024
+ // A completing task's call is over: its media has ended, or it never started. Audio still stated
1025
+ // as started on a task in wrap-up is a call whose audio never ended -- the customer heard through
1026
+ // the agent's notes -- and a task stating it contradicts itself.
1027
+ if (task.phase === "completing" && task.media === "started") {
1028
+ into.add("task.media.completing", `${path}.media`, "a completing task's media has ended or never started: the call is over, and media still started is audio that never ended");
1029
+ }
1024
1030
  if (Array.isArray(task.onCall) && task.onCall.length > 0 && (task.phase === "completing" || task.media === "ended")) {
1025
1031
  into.add("task.onCall.ended", `${path}.onCall`, `the call has ended (${task.phase === "completing" ? "the task is completing" : "its media ended"}) and onCall still names people on it: the room is who is on the call now, and now nobody is`);
1026
1032
  }
package/guide.md CHANGED
@@ -2854,7 +2854,12 @@ which the update itself shows: the party on the call carrying a `stage`. A compl
2854
2854
  republished as `in-progress` from a stale copy carries no such stage, and that is the ending the
2855
2855
  agent never saw. Audio arrives only on a task at work: `task-media-started` on a `completing` task
2856
2856
  is refused as it is on a pending one (`stream.taskMediaStarted.beforeWork`), since a connect-back
2857
- returns the task to `in-progress` before any media.
2857
+ returns the task to `in-progress` before any media. And a task completes after its audio ends,
2858
+ never around it: an update moving a task to `completing` while the stream holds its media as
2859
+ started is refused (`stream.taskUpdated.mediaOpen`), whoever caused the ending, and a task stating
2860
+ `completing` with `media: "started"` contradicts itself on any snapshot or update
2861
+ (`task.media.completing`). The consequence the rule exists for is concrete: the customer's audio
2862
+ keeps playing through the agent's wrap-up.
2858
2863
 
2859
2864
  What the agent is told differs by source, and only one source tells them anything. Under `queue`
2860
2865
  and `ungoverned` the agent sees controls and nothing about where they came from: both are facts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.71",
3
+ "version": "0.1.72",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",