@xema/omni-protocol 0.1.81 → 0.1.82

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  /** The protocol version implemented by this package. */
2
- export declare const OMNI_PROTOCOL_VERSION: 2;
2
+ export declare const OMNI_PROTOCOL_VERSION: 1;
3
3
  /** Every version this package can interoperate with. */
4
4
  export declare const OMNI_SUPPORTED_PROTOCOL_VERSIONS: readonly number[];
5
5
  /**
@@ -431,16 +431,13 @@ export type UrlVisibility = "full" | "domain" | "hidden";
431
431
  export type RecordingSource = "provider" | "host";
432
432
  export type RecordingAction = "start" | "pause" | "resume" | "stop" | "cancel";
433
433
  export declare const RECORDING_ACTIONS: readonly ["start", "pause", "resume", "stop", "cancel"];
434
- /** Cancel's captured-audio disposition is explicit; stop always retains captured audio. */
435
- export type RecordingCancelEffect = "retain" | "discard";
436
434
  export interface RecordingActions {
437
435
  start?: true;
438
436
  pause?: true;
439
437
  resume?: true;
440
438
  stop?: true;
441
- cancel?: {
442
- effect: RecordingCancelEffect;
443
- };
439
+ /** Abandon this recording and discard its captured audio. */
440
+ cancel?: true;
444
441
  }
445
442
  /** Per-task permission, not a provider-wide recording switch. Absence grants nothing. */
446
443
  export interface TaskRecordingPolicy {
@@ -477,15 +474,9 @@ export type RecordingCommand = {
477
474
  } & ({
478
475
  action: "start";
479
476
  recordingId?: never;
480
- cancelEffect?: never;
481
- } | {
482
- action: "pause" | "resume" | "stop";
483
- recordingId: string;
484
- cancelEffect?: never;
485
477
  } | {
486
- action: "cancel";
478
+ action: "pause" | "resume" | "stop" | "cancel";
487
479
  recordingId: string;
488
- cancelEffect: RecordingCancelEffect;
489
480
  });
490
481
  export interface HostRecordingReport {
491
482
  taskId: TaskId;
@@ -494,8 +485,9 @@ export interface HostRecordingReport {
494
485
  }
495
486
  /** Host declaration in ConnectContext, not the provider-owned Manifest. */
496
487
  export interface HostRecording {
488
+ /** Audible recording status announcements to the remote party, for host-owned capture only. */
489
+ announcesToCaller?: true;
497
490
  actions: RecordingAction[];
498
- cancelEffects: RecordingCancelEffect[];
499
491
  /** Explicitly provisioned destinations. A task chooses one; no upload/storage fallback. */
500
492
  destinationIds: string[];
501
493
  execute(request: HostRecordingRequest): Promise<RecordingCommandResult>;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Where this file and guide.md disagree, the guide is right and this is a defect.
4
4
  /** The protocol version implemented by this package. */
5
- export const OMNI_PROTOCOL_VERSION = 2;
5
+ export const OMNI_PROTOCOL_VERSION = 1;
6
6
  /** Every version this package can interoperate with. */
7
7
  export const OMNI_SUPPORTED_PROTOCOL_VERSIONS = [OMNI_PROTOCOL_VERSION];
8
8
  /**
package/dist/testing.js CHANGED
@@ -872,11 +872,11 @@ export function assertTaskCapabilityWithdrawal(tasks, manifest, command) {
872
872
  if (withdrawn.length === 0) {
873
873
  throw new Error("Task capability withdrawal must end with at least one capability the offer declared withdrawn; a locked control is present without permission, not withdrawn");
874
874
  }
875
- const before = validateTaskCommand(command, first, "command");
875
+ const before = validateTaskCommand(command, first, "command", context);
876
876
  if (before.length > 0) {
877
877
  throw new Error(`Task capability withdrawal: the command must be issuable against the task as offered, or its later refusal proves nothing (${before.map(v => v.rule).join(", ")})`);
878
878
  }
879
- const after = validateTaskCommand(command, last, "command");
879
+ const after = validateTaskCommand(command, last, "command", context);
880
880
  const forWant = after.filter(v => withdrawn.some(name => v.rule === `command.capability.${name}`));
881
881
  if (forWant.length === 0) {
882
882
  throw new Error(`Task capability withdrawal: the command must be refused against the republished task for want of ${withdrawn.join(" or ")}; it was ${after.length === 0 ? "accepted" : `refused for ${after.map(v => v.rule).join(", ")}`}`);
@@ -1560,7 +1560,11 @@ async function driveOneCall(drive) {
1560
1560
  // Every command the drive sends is validated against the task as published, and its answer for its method.
1561
1561
  ruleEvaluated("drive.timeout");
1562
1562
  const send = async (command, dialId) => {
1563
- const own = validateTaskCommand(command, latestTask(), `drive.command.${String(command.type)}`);
1563
+ const own = validateTaskCommand(command, latestTask(), `drive.command.${String(command.type)}`, {
1564
+ levels: effectiveLevels(drive.manifest.orgLevels).map(level => level.id),
1565
+ dialOutcomesDeclared: drive.manifest.dialOutcomes !== undefined,
1566
+ autoAcceptTasks: drive.context.autoAcceptTasks,
1567
+ });
1564
1568
  found.push(...own);
1565
1569
  if (own.length > 0)
1566
1570
  return undefined;
@@ -100,7 +100,7 @@ export declare function validateHostReport(report: unknown, path?: string): Prot
100
100
  * own shape is checked. A host validates before sending, and an adapter before acting: a command
101
101
  * for a control the task never offered is the host's error, and this names it.
102
102
  */
103
- export declare function validateTaskCommand(command: unknown, task?: unknown, path?: string): ProtocolViolation[];
103
+ export declare function validateTaskCommand(command: unknown, task?: unknown, path?: string, taskContext?: Omit<TaskValidationContext, "channel">): ProtocolViolation[];
104
104
  /** The connection methods whose results `validateResult` knows. */
105
105
  export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamLeadAssist" | "executeTeamMonitor" | "openMedia" | "setPreference" | "recordStep" | "executeTeamPolicy";
106
106
  /**
@@ -151,6 +151,8 @@ export declare function validateRecordingRequest(request: unknown, task: unknown
151
151
  host?: unknown;
152
152
  hostReport?: unknown;
153
153
  softphone?: boolean;
154
+ /** The same provider/task context used to validate the published task. */
155
+ taskContext?: Omit<TaskValidationContext, "channel">;
154
156
  }, path?: string): ProtocolViolation[];
155
157
  /** Check recording-specific result semantics against the owner's confirming observations. */
156
158
  export declare function validateRecordingOutcome(command: unknown, before: unknown, after: unknown, result: unknown, path?: string): ProtocolViolation[];
@@ -2027,7 +2027,7 @@ const CONFERENCE_ACTIONS = ["add", "remove"];
2027
2027
  * own shape is checked. A host validates before sending, and an adapter before acting: a command
2028
2028
  * for a control the task never offered is the host's error, and this names it.
2029
2029
  */
2030
- export function validateTaskCommand(command, task, path = "command") {
2030
+ export function validateTaskCommand(command, task, path = "command", taskContext) {
2031
2031
  const into = new Collector();
2032
2032
  if (!isPlainObject(command)) {
2033
2033
  into.add("command.shape", path, "a command must be an object");
@@ -2100,7 +2100,7 @@ export function validateTaskCommand(command, task, path = "command") {
2100
2100
  // checking against a task nobody has -- so the task is validated first, and a command is held
2101
2101
  // only to a task that stands.
2102
2102
  const published = new Collector();
2103
- validateTaskInto(task, { channel: channel ?? "voice" }, `${path}.task`, published);
2103
+ validateTaskInto(task, { ...taskContext, channel: channel ?? "voice" }, `${path}.task`, published);
2104
2104
  if (published.violations.length > 0 || !isPlainObject(task)) {
2105
2105
  into.add("command.task", `${path}.task`, `the task the command is held to is not one the wire published (${published.violations.map(v => v.rule).join(", ")}): pass the task as the provider sent it, or validate the command's shape alone`);
2106
2106
  return into.violations;
@@ -2173,7 +2173,8 @@ export function validateTaskCommand(command, task, path = "command") {
2173
2173
  handling();
2174
2174
  break;
2175
2175
  case "recording": {
2176
- offered("recording");
2176
+ if (!offered("recording"))
2177
+ break;
2177
2178
  const policy = isPlainObject(capabilities.recording) ? capabilities.recording.provider : undefined;
2178
2179
  const state = isPlainObject(task.recording) ? task.recording.provider : undefined;
2179
2180
  into.violations.push(...validateRecordingCommandState(command, policy, state, path));
@@ -2589,7 +2590,6 @@ export function validateAuthenticationState(state, path = "authentication", cont
2589
2590
  // Independent current recording evidence and dispatch validation.
2590
2591
  const object = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2591
2592
  const filled = (v) => typeof v === "string" && v.trim().length > 0;
2592
- const effects = ["retain", "discard"];
2593
2593
  function collector() {
2594
2594
  const violations = [];
2595
2595
  const check = (ok, rule, path, message) => {
@@ -2660,14 +2660,7 @@ export function validateRecordingPolicy(value, path = "capabilities.recording")
2660
2660
  for (const action of RECORDING_ACTIONS) {
2661
2661
  if (policy[action] === undefined)
2662
2662
  continue;
2663
- if (action === "cancel") {
2664
- const cancel = policy.cancel;
2665
- check(object(cancel) && effects.includes(cancel.effect), "cancel.effect", `${at}.cancel`, "cancel explicitly retains or discards this recording's captured audio");
2666
- if (object(cancel))
2667
- keys(cancel, ["effect"], `${at}.cancel`);
2668
- }
2669
- else
2670
- check(policy[action] === true, "policy.action", `${at}.${action}`, "permission is true or absent");
2663
+ check(policy[action] === true, "policy.action", `${at}.${action}`, "permission is true or absent");
2671
2664
  }
2672
2665
  if (source === "host")
2673
2666
  check(filled(policy.destinationId), "destination", `${at}.destinationId`, "host storage destination must be explicit per task");
@@ -2683,8 +2676,10 @@ export function validateHostRecording(value, softphone, path = "host.recording")
2683
2676
  check(false, "host.shape", path, "expected host recording declaration and executor");
2684
2677
  return violations;
2685
2678
  }
2686
- keys(value, ["actions", "cancelEffects", "destinationIds", "execute"], path);
2687
- for (const [key, allowed] of [["actions", RECORDING_ACTIONS], ["cancelEffects", effects], ["destinationIds", undefined]]) {
2679
+ keys(value, ["actions", "destinationIds", "execute", "announcesToCaller"], path);
2680
+ if (value.announcesToCaller !== undefined)
2681
+ check(value.announcesToCaller === true, "host.announcesToCaller", `${path}.announcesToCaller`, "caller announcement guarantee is true or absent");
2682
+ for (const [key, allowed] of [["actions", RECORDING_ACTIONS], ["destinationIds", undefined]]) {
2688
2683
  const list = value[key];
2689
2684
  if (!Array.isArray(list)) {
2690
2685
  check(false, "host.list", `${path}.${key}`, "expected explicit array");
@@ -2692,11 +2687,9 @@ export function validateHostRecording(value, softphone, path = "host.recording")
2692
2687
  }
2693
2688
  check(new Set(list).size === list.length, "host.duplicate", `${path}.${key}`, "duplicate declaration");
2694
2689
  check(list.every(item => filled(item) && (allowed === undefined || allowed.includes(item))), "host.list", `${path}.${key}`, "unsupported or empty declaration");
2695
- if (key !== "cancelEffects")
2696
- check(list.length > 0, "host.empty", `${path}.${key}`, "declare at least one supported value");
2690
+ check(list.length > 0, "host.empty", `${path}.${key}`, "declare at least one supported value");
2697
2691
  }
2698
- if (Array.isArray(value.actions) && Array.isArray(value.cancelEffects)) {
2699
- check(value.actions.includes("cancel") === (value.cancelEffects.length > 0), "host.cancel", path, "cancel support and explicit effects must agree");
2692
+ if (Array.isArray(value.actions)) {
2700
2693
  check(value.actions.includes("pause") === value.actions.includes("resume"), "host.pause", path, "a pausable host supports resume; task permissions may independently restrict it");
2701
2694
  }
2702
2695
  check(typeof value.execute === "function", "host.execute", `${path}.execute`, "host recording requires its own executor");
@@ -2732,32 +2725,47 @@ export function validateRecordingCommandShape(value, source, path = "command") {
2732
2725
  check(false, "command.shape", path, "expected recording command");
2733
2726
  return violations;
2734
2727
  }
2735
- keys(value, ["type", "source", "requestId", "observationId", "action", ...(value.action === "start" ? [] : ["recordingId"]), ...(value.action === "cancel" ? ["cancelEffect"] : [])], path);
2728
+ keys(value, ["type", "source", "requestId", "observationId", "action", ...(value.action === "start" ? [] : ["recordingId"])], path);
2736
2729
  check(value.type === "recording" && value.source === source, "command.source", path, `this executor accepts only ${source} recording commands`);
2737
2730
  check(RECORDING_ACTIONS.includes(value.action), "command.action", `${path}.action`, "unsupported recording action");
2738
2731
  for (const key of ["requestId", "observationId", ...(value.action === "start" ? [] : ["recordingId"])])
2739
2732
  check(filled(value[key]), "command.identity", `${path}.${key}`, "expected nonempty scoped identity");
2740
- if (value.action === "cancel")
2741
- check(effects.includes(value.cancelEffect), "cancel.effect", `${path}.cancelEffect`, "explicit captured-audio disposition required");
2742
2733
  return violations;
2743
2734
  }
2735
+ /** One transition table shared by dispatch checks and outcome checks. */
2736
+ const RECORDING_TRANSITIONS = {
2737
+ start: { from: ["inactive"], to: "active" },
2738
+ pause: { from: ["active"], to: "paused" },
2739
+ resume: { from: ["paused"], to: "active" },
2740
+ stop: { from: ["active", "paused"], to: "inactive" },
2741
+ cancel: { from: ["active", "paused"], to: "inactive" },
2742
+ };
2743
+ function recordingTransition(action) {
2744
+ return typeof action === "string" && Object.hasOwn(RECORDING_TRANSITIONS, action)
2745
+ ? RECORDING_TRANSITIONS[action] : undefined;
2746
+ }
2744
2747
  /** Structural/state permission check. Dispatch MUST also call validateRecordingRequest for scope and freshness. */
2745
2748
  export function validateRecordingCommandState(command, policy, state, path = "command") {
2746
2749
  const { violations, check } = collector();
2747
- if (!object(command))
2750
+ if (!object(command)) {
2751
+ check(false, "command.shape", path, "expected recording command");
2752
+ return violations;
2753
+ }
2754
+ const transition = recordingTransition(command.action);
2755
+ if (transition === undefined) {
2756
+ check(false, "command.action", `${path}.action`, "unsupported recording action");
2748
2757
  return violations;
2749
- check(object(policy) && policy[command.action] !== undefined, "command.permission", path, "task does not permit this action on this recorder");
2758
+ }
2759
+ const permitted = object(policy) && policy[command.action] === true;
2760
+ check(permitted, "command.permission", path, "task does not permit this action on this recorder");
2750
2761
  if (!object(state) || validateRecordingState(state).length) {
2751
2762
  check(false, "command.state", path, "no validated current recorder observation");
2752
2763
  return violations;
2753
2764
  }
2754
- const allowed = command.action === "start" ? ["inactive"] : command.action === "pause" ? ["active"] : command.action === "resume" ? ["paused"] : ["active", "paused"];
2755
- check(allowed.includes(state.status), "command.state", path, "action is invalid in the observed recorder state");
2765
+ check(transition.from.includes(state.status), "command.state", path, "action is invalid in the observed recorder state");
2756
2766
  check(command.observationId === state.observationId, "command.stale", path, "observation changed; reconcile before acting");
2757
2767
  if (command.action !== "start")
2758
2768
  check(command.recordingId === state.recordingId, "command.recordingId", path, "command targets another recording");
2759
- if (command.action === "cancel" && object(policy))
2760
- check(object(policy.cancel) && command.cancelEffect === policy.cancel.effect, "command.cancelEffect", path, "cancel disposition must match task policy");
2761
2769
  return violations;
2762
2770
  }
2763
2771
  /** Validate immediately before dispatch using current task, host report and trusted observer-domain time. */
@@ -2767,7 +2775,7 @@ export function validateRecordingRequest(request, task, context, path = "request
2767
2775
  check(false, "request.shape", path, "request and current task required");
2768
2776
  return violations;
2769
2777
  }
2770
- violations.push(...validateTask(task, { channel: "voice" }, `${path}.task`));
2778
+ violations.push(...validateTask(task, { ...context.taskContext, channel: "voice" }, `${path}.task`));
2771
2779
  keys(request, ["taskId", "allocationId", "command"], path);
2772
2780
  check(filled(request.taskId) && filled(request.allocationId) && request.taskId === task.id && request.allocationId === task.allocationId, "request.scope", path, "task allocation changed or scope missing");
2773
2781
  check(task.channel === "voice", "request.channel", path, "recording is voice-only");
@@ -2788,8 +2796,6 @@ export function validateRecordingRequest(request, task, context, path = "request
2788
2796
  if (object(context.host)) {
2789
2797
  check(Array.isArray(context.host.actions) && context.host.actions.includes(command.action), "host.action", path, "host does not support action");
2790
2798
  check(object(policy) && Array.isArray(context.host.destinationIds) && context.host.destinationIds.includes(policy.destinationId), "host.destination", path, "task destination is not provisioned by this host");
2791
- if (command.action === "cancel")
2792
- check(Array.isArray(context.host.cancelEffects) && context.host.cancelEffects.includes(command.cancelEffect), "host.cancelEffect", path, "host does not support cancel disposition");
2793
2799
  }
2794
2800
  }
2795
2801
  const fresh = effectiveRecordingState(state, context.now);
@@ -2819,12 +2825,13 @@ export function validateRecordingOutcome(command, before, after, result, path =
2819
2825
  check(before.status !== "unknown" && after.status === before.status && after.recordingId === before.recordingId, "outcome.noEffect", path, "failed promises confirmed no effect; ambiguous or partial effects have no result");
2820
2826
  }
2821
2827
  else if (result.status === "applied") {
2822
- const expected = command.action === "pause" ? "paused" : command.action === "start" || command.action === "resume" ? "active" : "inactive";
2823
- check(after.status === expected, "outcome.state", path, "applied requires the confirming requested state");
2828
+ const transition = recordingTransition(command.action);
2829
+ if (transition === undefined)
2830
+ return violations; // shape validation already reported the unknown action
2831
+ check(after.status === transition.to, "outcome.state", path, "applied requires the confirming requested state");
2824
2832
  check(after.observationId !== before.observationId, "outcome.observation", path, "an applied transition needs new confirming evidence");
2825
2833
  check(before.observationId === command.observationId, "outcome.stale", path, "command must act against its named observation");
2826
- const origin = command.action === "start" ? ["inactive"] : command.action === "pause" ? ["active"] : command.action === "resume" ? ["paused"] : ["active", "paused"];
2827
- check(origin.includes(before.status), "outcome.origin", path, "applied cannot claim an impossible transition");
2834
+ check(transition.from.includes(before.status), "outcome.origin", path, "applied cannot claim an impossible transition");
2828
2835
  if (command.action !== "start")
2829
2836
  check(command.recordingId === before.recordingId, "outcome.recordingId", path, "command must target the prior recording");
2830
2837
  if (command.action === "pause" || command.action === "resume")
package/guide.md CHANGED
@@ -58,7 +58,7 @@ transport, a task, a break and a call each have a word of their own:
58
58
 
59
59
  ### `OMNI_PROTOCOL_VERSION`
60
60
 
61
- The exact protocol version implemented by this package. The current value is `2`. Version 2 replaces the broad recording flag and untargeted recording command; version 1 is not negotiated by this package. Hosts and adapters must explicitly migrate together or retain their own version-1 implementation.
61
+ The exact protocol version implemented by this package. The current value remains `1` during pre-release development. Recording contract changes do not introduce a new protocol version. Hosts and adapters adopt the current declarations together; this does not provide compatibility mapping for older recording shapes.
62
62
 
63
63
  ### `Manifest.supportedProtocolVersions`
64
64
 
@@ -292,13 +292,12 @@ type UrlVisibility = "full" | "domain" | "hidden";
292
292
  type RecordingSource = "provider" | "host";
293
293
  type RecordingAction = "start" | "pause" | "resume" | "stop" | "cancel";
294
294
  const RECORDING_ACTIONS = ["start", "pause", "resume", "stop", "cancel"] as const satisfies readonly RecordingAction[];
295
- type RecordingCancelEffect = "retain" | "discard";
296
295
  interface RecordingActions {
297
296
  start?: true;
298
297
  pause?: true;
299
298
  resume?: true;
300
299
  stop?: true;
301
- cancel?: { effect: RecordingCancelEffect };
300
+ cancel?: true;
302
301
  }
303
302
  interface TaskRecordingPolicy {
304
303
  provider?: RecordingActions;
@@ -316,9 +315,8 @@ type RecordingCommand = {
316
315
  requestId: string;
317
316
  observationId: string;
318
317
  } & (
319
- | { action: "start"; recordingId?: never; cancelEffect?: never }
320
- | { action: "pause" | "resume" | "stop"; recordingId: string; cancelEffect?: never }
321
- | { action: "cancel"; recordingId: string; cancelEffect: RecordingCancelEffect }
318
+ | { action: "start"; recordingId?: never }
319
+ | { action: "pause" | "resume" | "stop" | "cancel"; recordingId: string }
322
320
  );
323
321
  interface HostRecordingReport {
324
322
  taskId: TaskId;
@@ -326,8 +324,8 @@ interface HostRecordingReport {
326
324
  state: RecordingState;
327
325
  }
328
326
  interface HostRecording {
327
+ announcesToCaller?: true;
329
328
  actions: RecordingAction[];
330
- cancelEffects: RecordingCancelEffect[];
331
329
  destinationIds: string[];
332
330
  execute(request: HostRecordingRequest): Promise<RecordingCommandResult>;
333
331
  }
@@ -4992,7 +4990,7 @@ capabilities and what its work is **called** through `phaseLabels` and `taskType
4992
4990
  any of it is drawn is Omni's. A task cannot select a design language, inject a component, or
4993
4991
  override the agent's theme and font preferences.
4994
4992
 
4995
- ## Independent task recording (protocol 2)
4993
+ ## Independent task recording
4996
4994
 
4997
4995
  Recording is voice-only. A task can arrive already recording, including while pending, and offer
4998
4996
  no recording controls. The provider publishes only its own current state on
@@ -5019,14 +5017,14 @@ This package declares that contract; it supplies no recorder, media mixing, stor
5019
5017
  | pause | active | paused, preserving recording identity and captured audio |
5020
5018
  | resume | paused | active with the same recording identity |
5021
5019
  | stop | active or paused | inactive; finish and retain captured audio |
5022
- | cancel | active or paused | inactive; abandon with the explicitly declared captured-audio disposition |
5023
-
5024
- Cancel must declare retain or discard in task policy and echo that exact effect in the command.
5025
- Retain abandons further capture while preserving captured audio; discard additionally requires
5026
- confirmed disposal of that recording's audio under the recorder's storage contract. Neither
5027
- means cancelling an in-flight start request or deleting arbitrary past recordings. A recorder
5028
- unable to guarantee the offered effect must not advertise it. Stop can be applied only after
5029
- finalization/retention succeeds; cancel-discard only after cessation and disposition succeed.
5020
+ | cancel | active or paused | inactive; abandon and discard captured audio |
5021
+
5022
+ Stop finishes the recording and retains captured audio. Cancel abandons the recording and discards
5023
+ its captured audio; the task policy offers it with `cancel: true`. There is no configurable cancel
5024
+ effect. A recorder that cannot confirm disposal must not offer Cancel; it can offer Stop instead.
5025
+ Cancel never means cancelling an in-flight start request or deleting arbitrary past recordings.
5026
+ Stop can be applied only after finalization/retention succeeds; Cancel only after both cessation
5027
+ and disposal of this recording's audio succeed under the recorder's storage contract.
5030
5028
  Partial success (capture stopped but storage outcome unknown) cannot return failed with a claim
5031
5029
  of no effect. It rejects with unknown outcome, reports the failure visibly and publishes whatever
5032
5030
  current capture state is actually known. Retention is not a promise of sample-perfect audio.
@@ -5075,8 +5073,9 @@ updates remain full current task/host views, not replayed provider events. These
5075
5073
  create no handling-history entries and infer no actors, durations or historical capture boundaries.
5076
5074
 
5077
5075
  Migration is explicit: replace the old true recording capability with per-path action policies,
5078
- replace untargeted commands with scoped commands, and declare protocol 2 only after implementing
5079
- these semantics. Existing version-1 adapters are refused by negotiation rather than silently mapped.
5076
+ replace untargeted commands with scoped commands, and retain protocol version 1. During pre-release
5077
+ development, hosts and adapters must align their recording contracts; negotiation does not detect
5078
+ differences between package revisions that share that protocol number. No legacy-shape fallback is provided.
5080
5079
  This change does not publish a package or enable recording in any existing host/provider by itself.
5081
5080
 
5082
5081
  `validateRecordingOutcome` checks confirming observations against recording-specific applied/failed
@@ -5088,3 +5087,48 @@ keep that binding through pause/resume/stop/cancel; a later policy cannot redire
5088
5087
  The executor rejects a mismatched destination rather than moving or discarding another binding.
5089
5088
  Action permission does not authorize unattended invocation: host controls require the agent's explicit
5090
5089
  act, and provider authorization remains enforced at its authenticated command boundary.
5090
+
5091
+
5092
+ Recording dispatch must receive the same known task-validation context as the published task.
5093
+ Pass organisation levels and other known restrictions through the optional fourth argument of
5094
+ `validateTaskCommand`, and through `taskContext` in `validateRecordingRequest`. This keeps a valid
5095
+ custom organisation lock from being rejected against the default ladder, and prevents known
5096
+ capability restrictions from disappearing at dispatch. The standalone `validateRecordingCommandState`
5097
+ requires an affirmative permission; false, malformed permission declarations and unknown actions grant nothing.
5098
+
5099
+
5100
+ ### Host recording announcements to the caller
5101
+
5102
+ `HostRecording.announcesToCaller`, when true, is a guarantee that the host delivers audible recording
5103
+ status messages to the remote party over the call's outgoing audio. It belongs inside the optional
5104
+ host recording capability: without that capability there is no such guarantee. Omission makes no
5105
+ promise; false is invalid. It does not belong in the provider manifest, the task's recording policy
5106
+ or the general `HostGuarantees` object. A provider can inspect this host guarantee when deciding
5107
+ whether to permit host recording on a task.
5108
+
5109
+ The guarantee applies only to host-owned recordings. Providers implement announcements for their
5110
+ own recordings at their end; provider state updates must not cause the host to announce those
5111
+ recordings. Both recorders can operate independently on the same call.
5112
+
5113
+ On a confirmed host start, the remote party hears that recording started. On a confirmed stop or
5114
+ cancel, they hear that recording stopped. If pause/resume is supported, say paused/resumed so a
5115
+ pause is not presented as a finished recording. Announcements follow actual capture transitions,
5116
+ not button clicks, accepted requests or task-policy changes. When attaching to an already-active
5117
+ host recording, announce that recording is active, without inventing its original start time.
5118
+ Repeated observations of the same state do not repeat announcements. Unknown or expired evidence
5119
+ must never produce a stopped announcement.
5120
+
5121
+ A local sound, UI indicator, screen-reader message to the agent, or sound leaking through the
5122
+ microphone does not fulfil this guarantee. The host must deliver the message directly in the audio
5123
+ sent to the remote party, even when the agent microphone is muted. It may also notify the agent.
5124
+ Only declare the guarantee when the host can provide that outgoing audio path. An announcement
5125
+ that cannot be delivered, including after the remote party has disconnected, is a visible failure;
5126
+ never claim delivery or replay the notice into a different call.
5127
+
5128
+ For a command under this guarantee, applied requires both the recording action and its announcement
5129
+ to succeed. If capture changes but announcement delivery fails, publish the actual recording state,
5130
+ report the announcement failure visibly and reject with unknown command outcome. Do not claim no
5131
+ effect, automatically repeat the recording action or silently switch to an agent-only notice.
5132
+
5133
+ `validateHostRecording` checks the declaration, including its true-or-absent guarantee. Actual audio
5134
+ delivery remains the host implementation's responsibility and must be exercised in its own tests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.81",
3
+ "version": "0.1.82",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",