@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 +6 -14
- package/dist/index.js +1 -1
- package/dist/testing.js +7 -3
- package/dist/validation.d.ts +3 -1
- package/dist/validation.js +41 -34
- package/guide.md +62 -18
- package/package.json +1 -1
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
|
+
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
|
-
|
|
442
|
-
|
|
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 =
|
|
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;
|
package/dist/validation.d.ts
CHANGED
|
@@ -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[];
|
package/dist/validation.js
CHANGED
|
@@ -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
|
-
|
|
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", "
|
|
2687
|
-
|
|
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
|
-
|
|
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)
|
|
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"])
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2823
|
-
|
|
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
|
-
|
|
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
|
|
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?:
|
|
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
|
|
320
|
-
| { action: "pause" | "resume" | "stop"; recordingId: string
|
|
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
|
|
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
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
means cancelling an in-flight start request or deleting arbitrary past recordings.
|
|
5028
|
-
|
|
5029
|
-
|
|
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
|
|
5079
|
-
|
|
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.
|