@xema/omni-protocol 0.1.110 → 0.1.112
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 -0
- package/dist/testing.d.ts +8 -0
- package/dist/testing.js +147 -34
- package/guide/agent.md +7 -2
- package/guide.md +23 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1662,6 +1662,12 @@ export type HistoryReport = {
|
|
|
1662
1662
|
ended?: never;
|
|
1663
1663
|
seconds?: DurationSeconds;
|
|
1664
1664
|
});
|
|
1665
|
+
/**
|
|
1666
|
+
* `at` is the provider's instant for the leg, on its own clock, assigned when the provider writes
|
|
1667
|
+
* the leg into its record and never an echo of the host's: the host keeps the answer as the leg's
|
|
1668
|
+
* identity from then on, and names it on the closing report. A provider that echoes the host's
|
|
1669
|
+
* instant has assigned nothing, and the drive names it (`drive.recordStep.identity`).
|
|
1670
|
+
*/
|
|
1665
1671
|
export type HistoryReportResult = {
|
|
1666
1672
|
status: "recorded";
|
|
1667
1673
|
at: IsoTimestamp;
|
package/dist/testing.d.ts
CHANGED
|
@@ -151,6 +151,14 @@ export declare class TaskStream {
|
|
|
151
151
|
private readonly endedAssignments;
|
|
152
152
|
/** Every assignment a `task-offered` introduced, as against one a snapshot carried: each is owed its ending. */
|
|
153
153
|
private readonly offered;
|
|
154
|
+
/** The last seconds-left value the stream read per task and field, with the provider instant it was published at. */
|
|
155
|
+
private readonly countdowns;
|
|
156
|
+
/**
|
|
157
|
+
* A seconds-left value is the provider's arithmetic at the instant of the publication, never a
|
|
158
|
+
* value copied from an earlier one: between two publications the countdown loses at least the
|
|
159
|
+
* seconds the provider's own clock says passed, within a second of rounding.
|
|
160
|
+
*/
|
|
161
|
+
private holdCountdowns;
|
|
154
162
|
private readonly dials;
|
|
155
163
|
/** The host placed a dial: its outcome, whenever it arrives, is one the stream expects. */
|
|
156
164
|
dialled(dialId: string): void;
|
package/dist/testing.js
CHANGED
|
@@ -515,8 +515,12 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
515
515
|
unsubscribeAuthentication = session.subscribe(onAuthenticationState);
|
|
516
516
|
unsubscribe = second.subscribe(onEnvelope);
|
|
517
517
|
clientLive = true;
|
|
518
|
+
// The second client has been told nothing: the switch is per connection, and the reader
|
|
519
|
+
// holds its connect snapshot to no team until it is sent again, after its capacity.
|
|
520
|
+
leadFeaturesOn = false;
|
|
518
521
|
const restated = await second.setCapacity({ count: capacityStated ?? 1 });
|
|
519
522
|
violations.push(...validateResult(restated, "setCapacity", "drive.reload.setCapacity"));
|
|
523
|
+
await switchOnLeadFeatures(second, "drive.reload");
|
|
520
524
|
},
|
|
521
525
|
};
|
|
522
526
|
// Every snapshot the run reads, the connect snapshot and a reloaded client's, is read the same
|
|
@@ -619,40 +623,44 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
619
623
|
}
|
|
620
624
|
// The provider assumes nothing of the team feature: the application says on every connect,
|
|
621
625
|
// and once it has said on, the whole team is owed, once. Before that nothing of the team
|
|
622
|
-
// reaches the lead, which the reader held the connect snapshot to.
|
|
623
|
-
|
|
626
|
+
// reaches the lead, which the reader held the connect snapshot to. A reloaded client is a
|
|
627
|
+
// connect too: it is told again, after its capacity, and held the same way.
|
|
628
|
+
const switchOnLeadFeatures = async (on, at) => {
|
|
629
|
+
if (current().capabilities.lead !== true || typeof on.executeTeam !== "function")
|
|
630
|
+
return;
|
|
624
631
|
const from = events.length;
|
|
625
632
|
let switched;
|
|
626
633
|
// On from the moment the application says so: the provider may answer with the team at once.
|
|
627
634
|
leadFeaturesOn = true;
|
|
628
635
|
try {
|
|
629
|
-
switched = await
|
|
636
|
+
switched = await on.executeTeam({ command: { type: "lead-features", enabled: true } });
|
|
630
637
|
}
|
|
631
638
|
catch (error) {
|
|
632
639
|
leadFeaturesOn = false;
|
|
633
|
-
violations.push({ rule: "team.switch.rejected", path:
|
|
640
|
+
violations.push({ rule: "team.switch.rejected", path: `${at}.executeTeam`, message: `lead-features on was rejected: ${String(error)}` });
|
|
634
641
|
}
|
|
635
|
-
if (switched
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
642
|
+
if (switched === undefined)
|
|
643
|
+
return;
|
|
644
|
+
violations.push(...validateResult(switched, "executeTeam", `${at}.executeTeam`));
|
|
645
|
+
ruleEvaluated("team.required");
|
|
646
|
+
const ms = options.driveTimeoutMs ?? 5000;
|
|
647
|
+
const isBaseline = (envelope) => isRecord(envelope?.event) && envelope.event.type === "team-updated";
|
|
648
|
+
const baseline = events.slice(from).some(isBaseline) || await new Promise(resolve => {
|
|
649
|
+
const timer = setTimeout(() => { waiters.delete(waiter); resolve(false); }, ms);
|
|
650
|
+
const waiter = (envelope) => {
|
|
651
|
+
if (!isBaseline(envelope))
|
|
652
|
+
return;
|
|
653
|
+
clearTimeout(timer);
|
|
654
|
+
waiters.delete(waiter);
|
|
655
|
+
resolve(true);
|
|
656
|
+
};
|
|
657
|
+
waiters.add(waiter);
|
|
658
|
+
});
|
|
659
|
+
if (!baseline)
|
|
660
|
+
violations.push({ rule: "team.required", path: "event.team",
|
|
661
|
+
message: `the application switched the team feature on and no team-updated followed within ${ms}ms: the whole team is owed once it is on, members: [] when nobody is in it` });
|
|
662
|
+
};
|
|
663
|
+
await switchOnLeadFeatures(live, "connection");
|
|
656
664
|
if (options.drive) {
|
|
657
665
|
const localAudio = isRecord(first) && isRecord(first.audio) && isRecord(first.audio.input) && first.audio.input.status === "available"
|
|
658
666
|
? first.audio.input.localAudio : undefined;
|
|
@@ -1131,6 +1139,36 @@ export class TaskStream {
|
|
|
1131
1139
|
endedAssignments = new Set();
|
|
1132
1140
|
/** Every assignment a `task-offered` introduced, as against one a snapshot carried: each is owed its ending. */
|
|
1133
1141
|
offered = new Set();
|
|
1142
|
+
/** The last seconds-left value the stream read per task and field, with the provider instant it was published at. */
|
|
1143
|
+
countdowns = new Map();
|
|
1144
|
+
/**
|
|
1145
|
+
* A seconds-left value is the provider's arithmetic at the instant of the publication, never a
|
|
1146
|
+
* value copied from an earlier one: between two publications the countdown loses at least the
|
|
1147
|
+
* seconds the provider's own clock says passed, within a second of rounding.
|
|
1148
|
+
*/
|
|
1149
|
+
holdCountdowns(id, task, publishedAt, at, refuse) {
|
|
1150
|
+
const atMs = typeof publishedAt === "string" ? Date.parse(publishedAt) : Number.NaN;
|
|
1151
|
+
if (!isRecord(task) || Number.isNaN(atMs))
|
|
1152
|
+
return;
|
|
1153
|
+
const known = this.countdowns.get(id) ?? new Map();
|
|
1154
|
+
for (const field of ["expiresInSeconds", "previewEndsInSeconds", "wrapEndsInSeconds"]) {
|
|
1155
|
+
const value = task[field];
|
|
1156
|
+
if (typeof value !== "number") {
|
|
1157
|
+
known.delete(field);
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
const before = known.get(field);
|
|
1161
|
+
if (before !== undefined) {
|
|
1162
|
+
ruleEvaluated("stream.countdown.copied");
|
|
1163
|
+
const elapsed = Math.floor((atMs - before.atMs) / 1000);
|
|
1164
|
+
if (elapsed > 0 && value > before.value - elapsed + 1) {
|
|
1165
|
+
refuse("stream.countdown.copied", `${at}.${field}`, `${id} said ${field} ${before.value} at ${new Date(before.atMs).toISOString()} and ${value} at ${new Date(atMs).toISOString()}: ${elapsed}s passed on the provider's clock and the countdown lost fewer; seconds left are worked out at each publication, never copied from the one before`);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
known.set(field, { value, atMs });
|
|
1169
|
+
}
|
|
1170
|
+
this.countdowns.set(id, known);
|
|
1171
|
+
}
|
|
1134
1172
|
// Every dial the stream can place an outcome against: one the host said it placed, or one a
|
|
1135
1173
|
// task carried on `onCall` or in its record -- which is how a dial made before a take-over is known
|
|
1136
1174
|
// to whoever holds the task now. `answered` or `ended` once its outcome arrived, since it comes once.
|
|
@@ -1232,6 +1270,11 @@ export class TaskStream {
|
|
|
1232
1270
|
snapshot.tasks.forEach((task, index) => {
|
|
1233
1271
|
if (!isRecord(task) || typeof task.assignmentId !== "string")
|
|
1234
1272
|
return;
|
|
1273
|
+
// A snapshot from a provider with a clock is a publication at providerTime; the countdowns it carries are held to it.
|
|
1274
|
+
if (typeof snapshot.providerTime === "string")
|
|
1275
|
+
this.holdCountdowns(task.assignmentId, task, snapshot.providerTime, `${at}.tasks[${index}]`, refuse);
|
|
1276
|
+
else
|
|
1277
|
+
this.countdowns.delete(task.assignmentId);
|
|
1235
1278
|
const was = this.tasks.get(task.assignmentId);
|
|
1236
1279
|
if (was === undefined)
|
|
1237
1280
|
return;
|
|
@@ -1300,6 +1343,7 @@ export class TaskStream {
|
|
|
1300
1343
|
this.offered.add(id);
|
|
1301
1344
|
this.tasks.set(id, TaskStream.stated(event.task));
|
|
1302
1345
|
this.noteDials(event.task);
|
|
1346
|
+
this.holdCountdowns(id, event.task, isRecord(envelope) ? envelope.occurredAt : undefined, `${at}.task`, refuse);
|
|
1303
1347
|
break;
|
|
1304
1348
|
}
|
|
1305
1349
|
case "task-updated":
|
|
@@ -1311,6 +1355,7 @@ export class TaskStream {
|
|
|
1311
1355
|
break;
|
|
1312
1356
|
}
|
|
1313
1357
|
// The rules about a known task are evaluated only once there is one.
|
|
1358
|
+
this.holdCountdowns(id, event.task, isRecord(envelope) ? envelope.occurredAt : undefined, `${at}.task`, refuse);
|
|
1314
1359
|
ruleEvaluated("stream.taskUpdated.capabilitySource", "stream.taskUpdated.phase", "stream.taskUpdated.audioOpen", "stream.taskUpdated.history", "stream.taskAudioEnded.follow", "stream.taskUpdated.audio", "stream.taskUpdated.stage", "stream.taskUpdated.stage.lingering");
|
|
1315
1360
|
// Terms once read stay read. A re-read that fails is not a new fact about the task, so the
|
|
1316
1361
|
// last statement stands and the failure is a diagnostic; not-yet-read is a place a task
|
|
@@ -1662,16 +1707,16 @@ async function driveOneCall(drive) {
|
|
|
1662
1707
|
task = offered.found;
|
|
1663
1708
|
cursor = offered.at;
|
|
1664
1709
|
}
|
|
1665
|
-
|
|
1710
|
+
let taskId = task.assignmentId;
|
|
1666
1711
|
const latestTask = () => task;
|
|
1667
|
-
const updated = (until, what) => waitFor(what, envelope => {
|
|
1712
|
+
const updated = (until, what, within) => waitFor(what, envelope => {
|
|
1668
1713
|
const event = envelope.event;
|
|
1669
1714
|
if ((event.type === "task-updated" || event.type === "task-offered") && isTask(event.task) && event.task.assignmentId === taskId) {
|
|
1670
1715
|
task = event.task;
|
|
1671
1716
|
return until(event.task) ? event.task : undefined;
|
|
1672
1717
|
}
|
|
1673
1718
|
return undefined;
|
|
1674
|
-
}, cursor).then(hit => { if (hit)
|
|
1719
|
+
}, cursor, within).then(hit => { if (hit)
|
|
1675
1720
|
cursor = hit.at; return hit; });
|
|
1676
1721
|
const ended = (within) => waitFor("a task-ended for the driven task", envelope => {
|
|
1677
1722
|
const event = envelope.event;
|
|
@@ -1714,6 +1759,40 @@ async function driveOneCall(drive) {
|
|
|
1714
1759
|
// Audio may arrive any time after the accept, before or after the task's own update says
|
|
1715
1760
|
// in-progress: the event is the provider's word that the audio should attach, never a reply to
|
|
1716
1761
|
// openAudio, so the drive looks for it from the accept rather than from the last update it read.
|
|
1762
|
+
// 0. A deadline stated is a deadline kept. Where the first offer says how long it has, within
|
|
1763
|
+
// what the drive will wait, the drive leaves it alone and expects the provider to end it expired
|
|
1764
|
+
// when it runs out; the next offer is the one driven. Offers with no deadline, or a longer one,
|
|
1765
|
+
// leave this rule unreached, and the result says so.
|
|
1766
|
+
const settleMs = Number(drive.manifest.settleMs);
|
|
1767
|
+
const lapse = latestTask().phase === "pending" ? latestTask().expiresInSeconds : undefined;
|
|
1768
|
+
if (typeof lapse === "number" && lapse * 1000 <= drive.timeoutMs) {
|
|
1769
|
+
ruleEvaluated("drive.offer.expired");
|
|
1770
|
+
const lapsedId = taskId;
|
|
1771
|
+
let missed = false;
|
|
1772
|
+
const ending = await waitFor(`the expired ending of ${lapsedId}`, envelope => {
|
|
1773
|
+
const event = envelope.event;
|
|
1774
|
+
return event.type === "task-ended" && event.assignmentId === lapsedId ? event : undefined;
|
|
1775
|
+
}, cursor, { ms: lapse * 1000 + settleMs, onExpiry: () => { missed = true; } });
|
|
1776
|
+
if (ending === undefined) {
|
|
1777
|
+
if (missed)
|
|
1778
|
+
refuse("drive.offer.expired", "drive.offer", `${lapsedId} was offered with ${lapse}s to answer and ${lapse * 1000 + settleMs}ms later it has not ended: a deadline stated is a deadline kept, and the offer ends expired naming pending`);
|
|
1779
|
+
return found;
|
|
1780
|
+
}
|
|
1781
|
+
const outcome = isRecord(ending.found.outcome) ? ending.found.outcome : {};
|
|
1782
|
+
if (!(outcome.type === "expired" && outcome.phase === "pending")) {
|
|
1783
|
+
refuse("drive.offer.expired", "drive.offer", `${lapsedId} lapsed unanswered and ended ${String(outcome.type)}: an offer nobody answered before its deadline ends expired naming pending`);
|
|
1784
|
+
}
|
|
1785
|
+
// The next offer is the one driven; a provider with nothing more to offer ends the drive here.
|
|
1786
|
+
const next = await waitFor("a task-offered after the lapsed one", envelope => {
|
|
1787
|
+
const event = envelope.event;
|
|
1788
|
+
return event.type === "task-offered" && isTask(event.task) && event.task.assignmentId !== lapsedId ? event.task : undefined;
|
|
1789
|
+
}, ending.at);
|
|
1790
|
+
if (next === undefined)
|
|
1791
|
+
return found;
|
|
1792
|
+
task = next.found;
|
|
1793
|
+
cursor = next.at;
|
|
1794
|
+
taskId = task.assignmentId;
|
|
1795
|
+
}
|
|
1717
1796
|
const acceptedAt = drive.events.length;
|
|
1718
1797
|
// 1. Accept the offer, if it is one.
|
|
1719
1798
|
if (latestTask().phase === "pending") {
|
|
@@ -1722,13 +1801,47 @@ async function driveOneCall(drive) {
|
|
|
1722
1801
|
if (await updated(t => t.phase !== "pending", "the task leaving pending after it was accepted") === undefined)
|
|
1723
1802
|
return found;
|
|
1724
1803
|
}
|
|
1725
|
-
// 2. A preview: press Call, which is a dial.
|
|
1804
|
+
// 2. A preview: press Call, which is a dial. Where the preview says how long it has, within what
|
|
1805
|
+
// the drive will wait, the deadline is kept to its atDeadline first: under provider-dials the
|
|
1806
|
+
// provider dials when it runs out and the drive presses nothing; under host-dials and waits the
|
|
1807
|
+
// preview stands until then, and the drive presses Call at zero as the desk would.
|
|
1726
1808
|
if (latestTask().phase === "preview") {
|
|
1727
|
-
const
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
if (
|
|
1809
|
+
const left = latestTask().previewEndsInSeconds;
|
|
1810
|
+
const atDeadline = latestTask().atDeadline;
|
|
1811
|
+
// Under provider-dials the provider places the call, and the desk presses nothing.
|
|
1812
|
+
let pressCall = true;
|
|
1813
|
+
if (typeof left === "number" && left * 1000 <= drive.timeoutMs && typeof atDeadline === "string") {
|
|
1814
|
+
ruleEvaluated("drive.preview.deadline");
|
|
1815
|
+
const ringing = (t) => t.phase !== "preview"
|
|
1816
|
+
|| (Array.isArray(t.onCall) && t.onCall.some(who => isRecord(who) && who.role === "party" && who.stage === "ringing"));
|
|
1817
|
+
if (atDeadline === "provider-dials") {
|
|
1818
|
+
let missed = false;
|
|
1819
|
+
const dialled = await updated(ringing, "the provider dialling at the preview's deadline", { ms: left * 1000 + settleMs, onExpiry: () => { missed = true; } });
|
|
1820
|
+
if (dialled === undefined) {
|
|
1821
|
+
if (!missed)
|
|
1822
|
+
return found;
|
|
1823
|
+
refuse("drive.preview.deadline", "drive.preview", `${taskId} is a preview under provider-dials with ${left}s left, and the provider did not dial when it ran out: the party rings on onCall, or the task leaves preview`);
|
|
1824
|
+
return found;
|
|
1825
|
+
}
|
|
1826
|
+
pressCall = false;
|
|
1827
|
+
}
|
|
1828
|
+
else {
|
|
1829
|
+
// Nothing may move the preview before its deadline under host-dials or waits: the drive
|
|
1830
|
+
// waits the seconds out and expects the task where it left it.
|
|
1831
|
+
const moved = await updated(t => t.phase !== "preview", "nothing", { ms: left * 1000, onExpiry: () => undefined });
|
|
1832
|
+
if (moved !== undefined) {
|
|
1833
|
+
refuse("drive.preview.deadline", "drive.preview", `${taskId} is a preview under ${atDeadline} with ${left}s left, and the provider moved it to ${String(moved.found.phase)} before the deadline: under ${atDeadline} the preview stands until the desk dials, or for as long as the agent needs`);
|
|
1834
|
+
return found;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
if (pressCall && latestTask().phase === "preview") {
|
|
1839
|
+
const dialId = `drive-${taskId}`;
|
|
1840
|
+
drive.stream.dialled(dialId);
|
|
1841
|
+
if (await send({ type: "dial", dialId }, dialId) === undefined)
|
|
1842
|
+
return found;
|
|
1843
|
+
}
|
|
1844
|
+
if (latestTask().phase === "preview" && await updated(t => t.phase === "in-progress" || t.phase === "completing", pressCall ? "the task leaving preview after Call" : "the task leaving preview after the provider's dial") === undefined)
|
|
1732
1845
|
return found;
|
|
1733
1846
|
}
|
|
1734
1847
|
// The other direction of step 4, wherever the task stands outside the interaction phases with the
|
package/guide/agent.md
CHANGED
|
@@ -588,8 +588,13 @@ as it begins and ends, and takes the provider's published entry as the closed ac
|
|
|
588
588
|
it does not reopen the leg, replace the provider's timestamp, or overwrite the provider's final
|
|
589
589
|
duration with a later report. A later agent mute is a new leg, never a reopening of the ended one.
|
|
590
590
|
|
|
591
|
-
|
|
592
|
-
|
|
591
|
+
**The provider assigns the instant.** `HistoryReportResult.at` is the provider's own instant for the
|
|
592
|
+
leg, on its own clock, written when the provider takes the leg into its record, never an echo of
|
|
593
|
+
the `at` the host reported: the host keeps the answer as the leg's identity from then on and names
|
|
594
|
+
it on the closing report, and a provider that echoes the host's instant has assigned nothing
|
|
595
|
+
(`drive.recordStep.identity`). Correlate the provider-published `muted` entry using that
|
|
596
|
+
provider-selected `at` acknowledged for the current report key; a final `seconds` closes that leg
|
|
597
|
+
under the history contract.
|
|
593
598
|
This is identity matching, not reconciliation between clocks. An old closed entry, an unrelated
|
|
594
599
|
task, or a stale connection cannot end a newer local mute. If publication precedes the acknowledgment,
|
|
595
600
|
retain the current provider view and apply the matching closure once correlation is available;
|
package/guide.md
CHANGED
|
@@ -2414,6 +2414,25 @@ and no count. A provider that publishes any of these instants therefore declares
|
|
|
2414
2414
|
(`manifest.timeCheck.required`) and states `providerTime` on every snapshot
|
|
2415
2415
|
(`snapshot.providerTime`). Two screens then differ by their delay and nothing else.
|
|
2416
2416
|
|
|
2417
|
+
**Seconds left are worked out at each publication.** `expiresInSeconds`, `previewEndsInSeconds`,
|
|
2418
|
+
`wrapEndsInSeconds` and `expectedEndsInSeconds` are each the provider's arithmetic at the instant
|
|
2419
|
+
of the publication that carries them, never a value copied from an earlier one. A provider that
|
|
2420
|
+
builds the next publication by spreading the last one forward hands a reloaded desk a countdown
|
|
2421
|
+
already spent -- sixty seconds shown beside a lead's screen showing twenty -- and the stream
|
|
2422
|
+
names it: between two publications of one countdown the value loses at least the seconds the
|
|
2423
|
+
provider's own clock says passed, within a second of rounding (`stream.countdown.copied`, on an
|
|
2424
|
+
update against the last publication, and on a snapshot from a provider with a clock against its
|
|
2425
|
+
`providerTime`). Under `agent-command` the wrap's value reaches `0` and stays there while the agent
|
|
2426
|
+
overruns; that is the true value, not a copy.
|
|
2427
|
+
|
|
2428
|
+
**A deadline stated is a deadline kept.** The provider that says how long an offer has ends it
|
|
2429
|
+
`expired` when it runs out, and the drive holds it to that: where the first offer carries
|
|
2430
|
+
`expiresInSeconds` within the drive's timeout, the drive leaves that offer unanswered and expects
|
|
2431
|
+
the `expired` ending within the seconds plus `settleMs`, then drives the next offer
|
|
2432
|
+
(`drive.offer.expired`). A preview's deadline is held to its `atDeadline` the same way: under
|
|
2433
|
+
`provider-dials` the provider dials when it runs out, under `host-dials` and `waits` the preview
|
|
2434
|
+
stands until the desk dials (`drive.preview.deadline`).
|
|
2435
|
+
|
|
2417
2436
|
#### Optional periodic provider time checks and agent application estimates
|
|
2418
2437
|
|
|
2419
2438
|
A provider may declare `Manifest.timeCheck: true` and implement `Connection.checkTime(request)`.
|
|
@@ -2976,7 +2995,10 @@ complete it with an outcome where the agent completes -- and holds every step to
|
|
|
2976
2995
|
agent application holds a provider to. Each command is validated against the task as published
|
|
2977
2996
|
(`drive.command.*`), each answer for its method, a refusal of a control the task offered is a
|
|
2978
2997
|
violation (`drive.command.failed`), and an event the provider owes and never sends is one too
|
|
2979
|
-
(`drive.timeout`, after `driveTimeoutMs`, 5000 by default).
|
|
2998
|
+
(`drive.timeout`, after `driveTimeoutMs`, 5000 by default). A first offer that says how long it has is left to
|
|
2999
|
+
lapse and expected to end `expired`, and a preview's deadline is held to its `atDeadline`
|
|
3000
|
+
(`drive.offer.expired`, `drive.preview.deadline`); see **Every screen counts from the provider's
|
|
3001
|
+
clock**. Around the drive the exercise holds
|
|
2980
3002
|
the run to what an agent application holds a provider to between commands: an offer before the agent application stated
|
|
2981
3003
|
capacity, or beyond the count with nothing dialled on it, is named
|
|
2982
3004
|
(`stream.taskOffered.beforeCapacity`, `.overCapacity`); every user the snapshot names is looked up
|