@frockbot/plugin-routines 0.0.0 → 0.1.0
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/frockbot.json +39 -0
- package/package.json +53 -6
- package/src/agent.test.ts +206 -0
- package/src/agent.ts +345 -0
- package/src/backend.test.ts +181 -0
- package/src/backend.ts +375 -0
- package/src/client/RoutineInboxBadge.vue +216 -0
- package/src/client/RoutinesSection.vue +601 -0
- package/src/client/RoutinesSummary.vue +150 -0
- package/src/client/index.test.ts +209 -0
- package/src/client/index.ts +289 -0
- package/src/client/state.ts +65 -0
- package/src/cron.test.ts +217 -0
- package/src/cron.ts +246 -0
- package/src/env.d.ts +6 -0
- package/src/firing.ts +222 -0
- package/src/hook.test.ts +394 -0
- package/src/hook.ts +405 -0
- package/src/inbox-store.ts +405 -0
- package/src/inbox.test.ts +402 -0
- package/src/inbox.ts +405 -0
- package/src/index.ts +9 -0
- package/src/manifest.ts +3 -0
- package/src/records.test.ts +138 -0
- package/src/records.ts +341 -0
- package/src/scheduler.test.ts +482 -0
- package/src/scheduler.ts +551 -0
- package/src/shared.test.ts +141 -0
- package/src/shared.ts +988 -0
- package/src/storage-keys.ts +202 -0
- package/src/store.test.ts +261 -0
- package/src/store.ts +789 -0
- package/src/testing.ts +55 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +31 -0
- package/README.md +0 -3
package/src/shared.ts
ADDED
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
// The Routines DTOs and commands: everything that crosses a runtime seam.
|
|
2
|
+
//
|
|
3
|
+
// "Cross-runtime communication uses narrow, versioned DTOs, and every inbound
|
|
4
|
+
// value is decoded at its seam." This module holds the wire shapes and their
|
|
5
|
+
// decoders and nothing else — no storage, no cron parsing (so the client bundle
|
|
6
|
+
// never carries a scheduler), and no key material of any kind.
|
|
7
|
+
//
|
|
8
|
+
// A `RoutineViewV1` never carries a webhook key or its digest. The key is minted
|
|
9
|
+
// once and shown once (D3); a listing is not a place to re-read a secret from.
|
|
10
|
+
import { canonicalCommandFingerprintV1 } from "@frockbot/configuration-core";
|
|
11
|
+
import {
|
|
12
|
+
decodeRoutineTriggerV1,
|
|
13
|
+
isRoutineIdV1,
|
|
14
|
+
requireScheduleXorTriggerV1,
|
|
15
|
+
RoutineDecodeError,
|
|
16
|
+
ROUTINE_NAME_MAX_LENGTH,
|
|
17
|
+
ROUTINE_PROMPT_MAX_LENGTH,
|
|
18
|
+
ROUTINE_RUN_STATUSES,
|
|
19
|
+
ROUTINE_SCHEDULE_MAX,
|
|
20
|
+
ROUTINE_TIMEZONE_MAX,
|
|
21
|
+
ROUTINE_TRIGGER_KINDS,
|
|
22
|
+
routineExactKeys,
|
|
23
|
+
routineText,
|
|
24
|
+
routineTimestamp,
|
|
25
|
+
type RoutineRunStatusV1,
|
|
26
|
+
type RoutineTriggerKindV1,
|
|
27
|
+
type RoutineTriggerV1,
|
|
28
|
+
} from "./records.js";
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
RoutineDecodeError,
|
|
32
|
+
ROUTINE_NAME_MAX_LENGTH,
|
|
33
|
+
ROUTINE_PROMPT_MAX_LENGTH,
|
|
34
|
+
ROUTINE_RUN_STATUSES,
|
|
35
|
+
ROUTINE_TRIGGER_KINDS,
|
|
36
|
+
} from "./records.js";
|
|
37
|
+
export type {
|
|
38
|
+
RoutineRunStatusV1,
|
|
39
|
+
RoutineTriggerKindV1,
|
|
40
|
+
RoutineTriggerV1,
|
|
41
|
+
RoutineWriterV1,
|
|
42
|
+
} from "./records.js";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The one path a webhook delivery is made to. It is built here so the route
|
|
46
|
+
* that answers it and the receipt that advertises it cannot drift apart.
|
|
47
|
+
*/
|
|
48
|
+
export function routineHookPathV1(botId: string, routineId: string): string {
|
|
49
|
+
return `/api/bots/${encodeURIComponent(botId)}/routines/${encodeURIComponent(routineId)}/hook`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Most Routines one listing carries. */
|
|
53
|
+
export const ROUTINE_LIST_MAX = 100;
|
|
54
|
+
/** Most run-log entries one listing carries. */
|
|
55
|
+
export const ROUTINE_RUN_LIST_MAX = 50;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Who wrote a Routine, as the client is told it. The Session and Turn a Bot
|
|
59
|
+
* writer records stay in the durable record: the UI needs to say "the Bot wrote
|
|
60
|
+
* this", not to replay the Turn that did.
|
|
61
|
+
*/
|
|
62
|
+
export type RoutineWriterViewV1 =
|
|
63
|
+
{ kind: "user" } | { kind: "bot"; botId: string };
|
|
64
|
+
|
|
65
|
+
/** One Routine as the hosted client sees it. Never any key material. */
|
|
66
|
+
export interface RoutineViewV1 {
|
|
67
|
+
schemaVersion: 1;
|
|
68
|
+
routineId: string;
|
|
69
|
+
name: string;
|
|
70
|
+
prompt: string;
|
|
71
|
+
schedule?: string;
|
|
72
|
+
trigger?: RoutineTriggerV1;
|
|
73
|
+
timezone: string;
|
|
74
|
+
enabled: boolean;
|
|
75
|
+
createdBy: RoutineWriterViewV1;
|
|
76
|
+
updatedBy: RoutineWriterViewV1;
|
|
77
|
+
createdAt: string;
|
|
78
|
+
updatedAt: string;
|
|
79
|
+
lastRunAt?: string;
|
|
80
|
+
/**
|
|
81
|
+
* When the scheduler will next fire this Routine. Absent for a webhook
|
|
82
|
+
* Routine, and for a paused one: the UI shows what the authority promised and
|
|
83
|
+
* nothing else.
|
|
84
|
+
*/
|
|
85
|
+
nextRunAt?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Whether a webhook key is live, and which version. Never the key, and never
|
|
88
|
+
* its digest: the UI needs to say "a key exists", not to re-read one.
|
|
89
|
+
*/
|
|
90
|
+
hookKeyVersion?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface RoutineListViewV1 {
|
|
94
|
+
schemaVersion: 1;
|
|
95
|
+
botId: string;
|
|
96
|
+
routines: RoutineViewV1[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** One firing as the hosted client sees it. */
|
|
100
|
+
export interface RoutineRunEntryViewV1 {
|
|
101
|
+
schemaVersion: 1;
|
|
102
|
+
entryId: string;
|
|
103
|
+
runId: string;
|
|
104
|
+
trigger: RoutineTriggerKindV1;
|
|
105
|
+
status: RoutineRunStatusV1;
|
|
106
|
+
startedAt: string;
|
|
107
|
+
finishedAt?: string;
|
|
108
|
+
summary?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface RoutineRunListViewV1 {
|
|
112
|
+
schemaVersion: 1;
|
|
113
|
+
botId: string;
|
|
114
|
+
routineId: string;
|
|
115
|
+
entries: RoutineRunEntryViewV1[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface RoutineCommandMetaV1 {
|
|
119
|
+
schemaVersion: 1;
|
|
120
|
+
commandId: string;
|
|
121
|
+
botId: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The Routine commands. They are Bot configuration commands in every sense that
|
|
126
|
+
* matters: one durable idempotency key per command, a fingerprint that makes a
|
|
127
|
+
* replay of the same key with different bytes an error, and one authority that
|
|
128
|
+
* applies them. They carry no `expectedRevision` because a Routine is its own
|
|
129
|
+
* durable record rather than a field of the Bot settings view — an unrelated
|
|
130
|
+
* profile edit must not make a Routine write conflict, and the reverse.
|
|
131
|
+
*/
|
|
132
|
+
export type RoutineCommandV1 =
|
|
133
|
+
| (RoutineCommandMetaV1 & {
|
|
134
|
+
type: "routine/create";
|
|
135
|
+
routineId?: string;
|
|
136
|
+
name: string;
|
|
137
|
+
prompt: string;
|
|
138
|
+
schedule?: string;
|
|
139
|
+
trigger?: RoutineTriggerV1;
|
|
140
|
+
timezone?: string;
|
|
141
|
+
})
|
|
142
|
+
| (RoutineCommandMetaV1 & {
|
|
143
|
+
/** Partial: only the fields the command carries change. */
|
|
144
|
+
type: "routine/update";
|
|
145
|
+
routineId: string;
|
|
146
|
+
name?: string;
|
|
147
|
+
prompt?: string;
|
|
148
|
+
schedule?: string;
|
|
149
|
+
trigger?: RoutineTriggerV1;
|
|
150
|
+
timezone?: string;
|
|
151
|
+
enabled?: boolean;
|
|
152
|
+
})
|
|
153
|
+
| (RoutineCommandMetaV1 & { type: "routine/pause"; routineId: string })
|
|
154
|
+
| (RoutineCommandMetaV1 & { type: "routine/resume"; routineId: string })
|
|
155
|
+
| (RoutineCommandMetaV1 & { type: "routine/delete"; routineId: string })
|
|
156
|
+
/**
|
|
157
|
+
* Fire now, out of band. It enqueues a firing rather than running one: the
|
|
158
|
+
* caller may itself be a Turn in flight, and a Routine is never run in
|
|
159
|
+
* parallel with anything. The alarm drains it.
|
|
160
|
+
*/
|
|
161
|
+
| (RoutineCommandMetaV1 & { type: "routine/run"; routineId: string })
|
|
162
|
+
/**
|
|
163
|
+
* Mint a fresh webhook key, retiring the one before it. The plaintext key
|
|
164
|
+
* comes back once, on the receipt, and is never stored or listed.
|
|
165
|
+
*/
|
|
166
|
+
| (RoutineCommandMetaV1 & { type: "routine/rotate-key"; routineId: string })
|
|
167
|
+
/** Retire the webhook key without minting another. Deliveries then 401. */
|
|
168
|
+
| (RoutineCommandMetaV1 & { type: "routine/revoke-key"; routineId: string });
|
|
169
|
+
|
|
170
|
+
export type RoutineCommandTypeV1 = RoutineCommandV1["type"];
|
|
171
|
+
|
|
172
|
+
export const ROUTINE_COMMAND_TYPES: readonly RoutineCommandTypeV1[] = [
|
|
173
|
+
"routine/create",
|
|
174
|
+
"routine/update",
|
|
175
|
+
"routine/pause",
|
|
176
|
+
"routine/resume",
|
|
177
|
+
"routine/delete",
|
|
178
|
+
"routine/run",
|
|
179
|
+
"routine/rotate-key",
|
|
180
|
+
"routine/revoke-key",
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* A freshly minted webhook key, handed back exactly once.
|
|
185
|
+
*
|
|
186
|
+
* It is on the receipt the caller receives and not on the receipt the Bot
|
|
187
|
+
* stores: a key that could be re-read from durable state would not be a secret,
|
|
188
|
+
* and a replayed command id therefore answers without one.
|
|
189
|
+
*/
|
|
190
|
+
export interface RoutineHookMintV1 {
|
|
191
|
+
schemaVersion: 1;
|
|
192
|
+
routineId: string;
|
|
193
|
+
keyVersion: number;
|
|
194
|
+
/** The plaintext key. Shown once; the Bot keeps only its digest. */
|
|
195
|
+
token: string;
|
|
196
|
+
/** The path the key is presented against, for the client to make a URL of. */
|
|
197
|
+
path: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export type RoutineCommandReceiptV1 =
|
|
201
|
+
| {
|
|
202
|
+
schemaVersion: 1;
|
|
203
|
+
commandId: string;
|
|
204
|
+
status: "applied";
|
|
205
|
+
routine: RoutineViewV1;
|
|
206
|
+
hook?: RoutineHookMintV1;
|
|
207
|
+
}
|
|
208
|
+
| {
|
|
209
|
+
schemaVersion: 1;
|
|
210
|
+
commandId: string;
|
|
211
|
+
status: "deleted";
|
|
212
|
+
routineId: string;
|
|
213
|
+
}
|
|
214
|
+
| {
|
|
215
|
+
schemaVersion: 1;
|
|
216
|
+
commandId: string;
|
|
217
|
+
status: "fired";
|
|
218
|
+
routineId: string;
|
|
219
|
+
/** The run id the firing will be admitted under. */
|
|
220
|
+
fireId: string;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The idempotency fingerprint of a Routine command: the same canonicalization
|
|
225
|
+
* `configurationCommandFingerprintV1` uses, under its own namespace, so a
|
|
226
|
+
* Routine command and a settings command can never collide.
|
|
227
|
+
*/
|
|
228
|
+
export function routineCommandFingerprintV1(command: RoutineCommandV1): string {
|
|
229
|
+
const { commandId: _commandId, ...semantic } = command;
|
|
230
|
+
return canonicalCommandFingerprintV1("routine-command-v1", semantic);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
234
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
235
|
+
throw new RoutineDecodeError(`${label} must be an object`);
|
|
236
|
+
}
|
|
237
|
+
return value as Record<string, unknown>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function commandIdentifier(value: unknown, label: string): string {
|
|
241
|
+
if (!isRoutineIdV1(value)) {
|
|
242
|
+
throw new RoutineDecodeError(`${label} is invalid`);
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function decodeRoutineCommandV1(value: unknown): RoutineCommandV1 {
|
|
248
|
+
const candidate = record(value, "Routine command");
|
|
249
|
+
if (candidate.schemaVersion !== 1) {
|
|
250
|
+
throw new RoutineDecodeError(
|
|
251
|
+
"Routine command schemaVersion is unsupported",
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
const type = ROUTINE_COMMAND_TYPES.find((known) => known === candidate.type);
|
|
255
|
+
if (!type) {
|
|
256
|
+
throw new RoutineDecodeError("Routine command type is unknown");
|
|
257
|
+
}
|
|
258
|
+
const meta: RoutineCommandMetaV1 = {
|
|
259
|
+
schemaVersion: 1,
|
|
260
|
+
commandId: commandIdentifier(candidate.commandId, "Routine commandId"),
|
|
261
|
+
botId: commandIdentifier(candidate.botId, "Routine command botId"),
|
|
262
|
+
};
|
|
263
|
+
if (type === "routine/create") {
|
|
264
|
+
routineExactKeys(
|
|
265
|
+
candidate,
|
|
266
|
+
["schemaVersion", "type", "commandId", "botId", "name", "prompt"],
|
|
267
|
+
["routineId", "schedule", "trigger", "timezone"],
|
|
268
|
+
"routine/create",
|
|
269
|
+
);
|
|
270
|
+
const created = {
|
|
271
|
+
...meta,
|
|
272
|
+
type,
|
|
273
|
+
name: routineText(
|
|
274
|
+
candidate.name,
|
|
275
|
+
ROUTINE_NAME_MAX_LENGTH,
|
|
276
|
+
"Routine name",
|
|
277
|
+
),
|
|
278
|
+
prompt: routineText(
|
|
279
|
+
candidate.prompt,
|
|
280
|
+
ROUTINE_PROMPT_MAX_LENGTH,
|
|
281
|
+
"Routine prompt",
|
|
282
|
+
),
|
|
283
|
+
...(candidate.routineId === undefined
|
|
284
|
+
? {}
|
|
285
|
+
: {
|
|
286
|
+
routineId: commandIdentifier(candidate.routineId, "Routine id"),
|
|
287
|
+
}),
|
|
288
|
+
...(candidate.schedule === undefined
|
|
289
|
+
? {}
|
|
290
|
+
: {
|
|
291
|
+
schedule: routineText(
|
|
292
|
+
candidate.schedule,
|
|
293
|
+
ROUTINE_SCHEDULE_MAX,
|
|
294
|
+
"Routine schedule",
|
|
295
|
+
),
|
|
296
|
+
}),
|
|
297
|
+
...(candidate.trigger === undefined
|
|
298
|
+
? {}
|
|
299
|
+
: { trigger: decodeRoutineTriggerV1(candidate.trigger) }),
|
|
300
|
+
...(candidate.timezone === undefined
|
|
301
|
+
? {}
|
|
302
|
+
: {
|
|
303
|
+
timezone: routineText(
|
|
304
|
+
candidate.timezone,
|
|
305
|
+
ROUTINE_TIMEZONE_MAX,
|
|
306
|
+
"Routine timezone",
|
|
307
|
+
),
|
|
308
|
+
}),
|
|
309
|
+
} satisfies Extract<RoutineCommandV1, { type: "routine/create" }>;
|
|
310
|
+
requireScheduleXorTriggerV1(created);
|
|
311
|
+
return created;
|
|
312
|
+
}
|
|
313
|
+
if (type === "routine/update") {
|
|
314
|
+
routineExactKeys(
|
|
315
|
+
candidate,
|
|
316
|
+
["schemaVersion", "type", "commandId", "botId", "routineId"],
|
|
317
|
+
["name", "prompt", "schedule", "trigger", "timezone", "enabled"],
|
|
318
|
+
"routine/update",
|
|
319
|
+
);
|
|
320
|
+
if (candidate.schedule !== undefined && candidate.trigger !== undefined) {
|
|
321
|
+
throw new RoutineDecodeError(
|
|
322
|
+
"a Routine carries a schedule or a trigger, never both",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (
|
|
326
|
+
candidate.enabled !== undefined &&
|
|
327
|
+
typeof candidate.enabled !== "boolean"
|
|
328
|
+
) {
|
|
329
|
+
throw new RoutineDecodeError("routine/update enabled must be a boolean");
|
|
330
|
+
}
|
|
331
|
+
const patched = {
|
|
332
|
+
...meta,
|
|
333
|
+
type,
|
|
334
|
+
routineId: commandIdentifier(candidate.routineId, "Routine id"),
|
|
335
|
+
...(candidate.name === undefined
|
|
336
|
+
? {}
|
|
337
|
+
: {
|
|
338
|
+
name: routineText(
|
|
339
|
+
candidate.name,
|
|
340
|
+
ROUTINE_NAME_MAX_LENGTH,
|
|
341
|
+
"Routine name",
|
|
342
|
+
),
|
|
343
|
+
}),
|
|
344
|
+
...(candidate.prompt === undefined
|
|
345
|
+
? {}
|
|
346
|
+
: {
|
|
347
|
+
prompt: routineText(
|
|
348
|
+
candidate.prompt,
|
|
349
|
+
ROUTINE_PROMPT_MAX_LENGTH,
|
|
350
|
+
"Routine prompt",
|
|
351
|
+
),
|
|
352
|
+
}),
|
|
353
|
+
...(candidate.schedule === undefined
|
|
354
|
+
? {}
|
|
355
|
+
: {
|
|
356
|
+
schedule: routineText(
|
|
357
|
+
candidate.schedule,
|
|
358
|
+
ROUTINE_SCHEDULE_MAX,
|
|
359
|
+
"Routine schedule",
|
|
360
|
+
),
|
|
361
|
+
}),
|
|
362
|
+
...(candidate.trigger === undefined
|
|
363
|
+
? {}
|
|
364
|
+
: { trigger: decodeRoutineTriggerV1(candidate.trigger) }),
|
|
365
|
+
...(candidate.timezone === undefined
|
|
366
|
+
? {}
|
|
367
|
+
: {
|
|
368
|
+
timezone: routineText(
|
|
369
|
+
candidate.timezone,
|
|
370
|
+
ROUTINE_TIMEZONE_MAX,
|
|
371
|
+
"Routine timezone",
|
|
372
|
+
),
|
|
373
|
+
}),
|
|
374
|
+
...(candidate.enabled === undefined
|
|
375
|
+
? {}
|
|
376
|
+
: { enabled: candidate.enabled as boolean }),
|
|
377
|
+
} satisfies Extract<RoutineCommandV1, { type: "routine/update" }>;
|
|
378
|
+
const changes = Object.keys(patched).filter(
|
|
379
|
+
(key) =>
|
|
380
|
+
key !== "schemaVersion" &&
|
|
381
|
+
key !== "type" &&
|
|
382
|
+
key !== "commandId" &&
|
|
383
|
+
key !== "botId" &&
|
|
384
|
+
key !== "routineId",
|
|
385
|
+
);
|
|
386
|
+
if (changes.length === 0) {
|
|
387
|
+
throw new RoutineDecodeError("routine/update changes nothing");
|
|
388
|
+
}
|
|
389
|
+
return patched;
|
|
390
|
+
}
|
|
391
|
+
routineExactKeys(
|
|
392
|
+
candidate,
|
|
393
|
+
["schemaVersion", "type", "commandId", "botId", "routineId"],
|
|
394
|
+
[],
|
|
395
|
+
type,
|
|
396
|
+
);
|
|
397
|
+
return {
|
|
398
|
+
...meta,
|
|
399
|
+
type,
|
|
400
|
+
routineId: commandIdentifier(candidate.routineId, "Routine id"),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function hookKeyVersion(value: unknown): number {
|
|
405
|
+
if (!Number.isSafeInteger(value) || (value as number) < 1) {
|
|
406
|
+
throw new RoutineDecodeError("Routine view hookKeyVersion is invalid");
|
|
407
|
+
}
|
|
408
|
+
return value as number;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function decodeRoutineWriterViewV1(
|
|
412
|
+
value: unknown,
|
|
413
|
+
label: string,
|
|
414
|
+
): RoutineWriterViewV1 {
|
|
415
|
+
const candidate = record(value, label);
|
|
416
|
+
if (candidate.kind === "user") {
|
|
417
|
+
routineExactKeys(candidate, ["kind"], [], label);
|
|
418
|
+
return { kind: "user" };
|
|
419
|
+
}
|
|
420
|
+
if (candidate.kind !== "bot") {
|
|
421
|
+
throw new RoutineDecodeError(`${label} kind is invalid`);
|
|
422
|
+
}
|
|
423
|
+
routineExactKeys(candidate, ["kind", "botId"], [], label);
|
|
424
|
+
return {
|
|
425
|
+
kind: "bot",
|
|
426
|
+
botId: routineText(candidate.botId, 128, `${label} botId`),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function decodeRoutineViewV1(value: unknown): RoutineViewV1 {
|
|
431
|
+
const candidate = record(value, "Routine view");
|
|
432
|
+
routineExactKeys(
|
|
433
|
+
candidate,
|
|
434
|
+
[
|
|
435
|
+
"schemaVersion",
|
|
436
|
+
"routineId",
|
|
437
|
+
"name",
|
|
438
|
+
"prompt",
|
|
439
|
+
"timezone",
|
|
440
|
+
"enabled",
|
|
441
|
+
"createdBy",
|
|
442
|
+
"updatedBy",
|
|
443
|
+
"createdAt",
|
|
444
|
+
"updatedAt",
|
|
445
|
+
],
|
|
446
|
+
["schedule", "trigger", "lastRunAt", "nextRunAt", "hookKeyVersion"],
|
|
447
|
+
"Routine view",
|
|
448
|
+
);
|
|
449
|
+
if (candidate.schemaVersion !== 1) {
|
|
450
|
+
throw new RoutineDecodeError("Routine view schemaVersion is unsupported");
|
|
451
|
+
}
|
|
452
|
+
if (typeof candidate.enabled !== "boolean") {
|
|
453
|
+
throw new RoutineDecodeError("Routine view enabled must be a boolean");
|
|
454
|
+
}
|
|
455
|
+
const view: RoutineViewV1 = {
|
|
456
|
+
schemaVersion: 1,
|
|
457
|
+
routineId: commandIdentifier(candidate.routineId, "Routine view routineId"),
|
|
458
|
+
name: routineText(candidate.name, ROUTINE_NAME_MAX_LENGTH, "Routine name"),
|
|
459
|
+
prompt: routineText(
|
|
460
|
+
candidate.prompt,
|
|
461
|
+
ROUTINE_PROMPT_MAX_LENGTH,
|
|
462
|
+
"Routine prompt",
|
|
463
|
+
),
|
|
464
|
+
timezone: routineText(
|
|
465
|
+
candidate.timezone,
|
|
466
|
+
ROUTINE_TIMEZONE_MAX,
|
|
467
|
+
"Routine timezone",
|
|
468
|
+
),
|
|
469
|
+
enabled: candidate.enabled,
|
|
470
|
+
createdBy: decodeRoutineWriterViewV1(
|
|
471
|
+
candidate.createdBy,
|
|
472
|
+
"Routine createdBy",
|
|
473
|
+
),
|
|
474
|
+
updatedBy: decodeRoutineWriterViewV1(
|
|
475
|
+
candidate.updatedBy,
|
|
476
|
+
"Routine updatedBy",
|
|
477
|
+
),
|
|
478
|
+
createdAt: routineTimestamp(candidate.createdAt, "Routine createdAt"),
|
|
479
|
+
updatedAt: routineTimestamp(candidate.updatedAt, "Routine updatedAt"),
|
|
480
|
+
...(candidate.schedule === undefined
|
|
481
|
+
? {}
|
|
482
|
+
: {
|
|
483
|
+
schedule: routineText(
|
|
484
|
+
candidate.schedule,
|
|
485
|
+
ROUTINE_SCHEDULE_MAX,
|
|
486
|
+
"Routine schedule",
|
|
487
|
+
),
|
|
488
|
+
}),
|
|
489
|
+
...(candidate.trigger === undefined
|
|
490
|
+
? {}
|
|
491
|
+
: { trigger: decodeRoutineTriggerV1(candidate.trigger) }),
|
|
492
|
+
...(candidate.lastRunAt === undefined
|
|
493
|
+
? {}
|
|
494
|
+
: {
|
|
495
|
+
lastRunAt: routineTimestamp(candidate.lastRunAt, "Routine lastRunAt"),
|
|
496
|
+
}),
|
|
497
|
+
...(candidate.nextRunAt === undefined
|
|
498
|
+
? {}
|
|
499
|
+
: {
|
|
500
|
+
nextRunAt: routineTimestamp(candidate.nextRunAt, "Routine nextRunAt"),
|
|
501
|
+
}),
|
|
502
|
+
...(candidate.hookKeyVersion === undefined
|
|
503
|
+
? {}
|
|
504
|
+
: { hookKeyVersion: hookKeyVersion(candidate.hookKeyVersion) }),
|
|
505
|
+
};
|
|
506
|
+
requireScheduleXorTriggerV1(view);
|
|
507
|
+
return view;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function decodeRoutineListViewV1(value: unknown): RoutineListViewV1 {
|
|
511
|
+
const candidate = record(value, "Routine list");
|
|
512
|
+
routineExactKeys(
|
|
513
|
+
candidate,
|
|
514
|
+
["schemaVersion", "botId", "routines"],
|
|
515
|
+
[],
|
|
516
|
+
"Routine list",
|
|
517
|
+
);
|
|
518
|
+
if (candidate.schemaVersion !== 1) {
|
|
519
|
+
throw new RoutineDecodeError("Routine list schemaVersion is unsupported");
|
|
520
|
+
}
|
|
521
|
+
if (!Array.isArray(candidate.routines)) {
|
|
522
|
+
throw new RoutineDecodeError("Routine list routines must be an array");
|
|
523
|
+
}
|
|
524
|
+
if (candidate.routines.length > ROUTINE_LIST_MAX) {
|
|
525
|
+
throw new RoutineDecodeError("Routine list is over its bound");
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
schemaVersion: 1,
|
|
529
|
+
botId: commandIdentifier(candidate.botId, "Routine list botId"),
|
|
530
|
+
routines: candidate.routines.map(decodeRoutineViewV1),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function decodeRoutineRunEntryViewV1(
|
|
535
|
+
value: unknown,
|
|
536
|
+
): RoutineRunEntryViewV1 {
|
|
537
|
+
const candidate = record(value, "Routine run entry view");
|
|
538
|
+
routineExactKeys(
|
|
539
|
+
candidate,
|
|
540
|
+
["schemaVersion", "entryId", "runId", "trigger", "status", "startedAt"],
|
|
541
|
+
["finishedAt", "summary"],
|
|
542
|
+
"Routine run entry view",
|
|
543
|
+
);
|
|
544
|
+
if (candidate.schemaVersion !== 1) {
|
|
545
|
+
throw new RoutineDecodeError(
|
|
546
|
+
"Routine run entry view schemaVersion is unsupported",
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
const status = ROUTINE_RUN_STATUSES.find(
|
|
550
|
+
(known) => known === candidate.status,
|
|
551
|
+
);
|
|
552
|
+
const trigger = ROUTINE_TRIGGER_KINDS.find(
|
|
553
|
+
(known) => known === candidate.trigger,
|
|
554
|
+
);
|
|
555
|
+
if (!status) {
|
|
556
|
+
throw new RoutineDecodeError("Routine run entry view status is invalid");
|
|
557
|
+
}
|
|
558
|
+
if (!trigger) {
|
|
559
|
+
throw new RoutineDecodeError("Routine run entry view trigger is invalid");
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
schemaVersion: 1,
|
|
563
|
+
entryId: routineText(candidate.entryId, 128, "Routine run entryId"),
|
|
564
|
+
runId: routineText(candidate.runId, 256, "Routine run runId"),
|
|
565
|
+
trigger,
|
|
566
|
+
status,
|
|
567
|
+
startedAt: routineTimestamp(candidate.startedAt, "Routine run startedAt"),
|
|
568
|
+
...(candidate.finishedAt === undefined
|
|
569
|
+
? {}
|
|
570
|
+
: {
|
|
571
|
+
finishedAt: routineTimestamp(
|
|
572
|
+
candidate.finishedAt,
|
|
573
|
+
"Routine run finishedAt",
|
|
574
|
+
),
|
|
575
|
+
}),
|
|
576
|
+
...(candidate.summary === undefined
|
|
577
|
+
? {}
|
|
578
|
+
: {
|
|
579
|
+
summary: routineText(candidate.summary, 2_000, "Routine run summary"),
|
|
580
|
+
}),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function decodeRoutineRunListViewV1(
|
|
585
|
+
value: unknown,
|
|
586
|
+
): RoutineRunListViewV1 {
|
|
587
|
+
const candidate = record(value, "Routine run list");
|
|
588
|
+
routineExactKeys(
|
|
589
|
+
candidate,
|
|
590
|
+
["schemaVersion", "botId", "routineId", "entries"],
|
|
591
|
+
[],
|
|
592
|
+
"Routine run list",
|
|
593
|
+
);
|
|
594
|
+
if (candidate.schemaVersion !== 1) {
|
|
595
|
+
throw new RoutineDecodeError(
|
|
596
|
+
"Routine run list schemaVersion is unsupported",
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
if (!Array.isArray(candidate.entries)) {
|
|
600
|
+
throw new RoutineDecodeError("Routine run list entries must be an array");
|
|
601
|
+
}
|
|
602
|
+
if (candidate.entries.length > ROUTINE_RUN_LIST_MAX) {
|
|
603
|
+
throw new RoutineDecodeError("Routine run list is over its bound");
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
schemaVersion: 1,
|
|
607
|
+
botId: commandIdentifier(candidate.botId, "Routine run list botId"),
|
|
608
|
+
routineId: commandIdentifier(
|
|
609
|
+
candidate.routineId,
|
|
610
|
+
"Routine run list routineId",
|
|
611
|
+
),
|
|
612
|
+
entries: candidate.entries.map(decodeRoutineRunEntryViewV1),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function decodeRoutineHookMintV1(value: unknown): RoutineHookMintV1 {
|
|
617
|
+
const candidate = record(value, "Routine hook mint");
|
|
618
|
+
routineExactKeys(
|
|
619
|
+
candidate,
|
|
620
|
+
["schemaVersion", "routineId", "keyVersion", "token", "path"],
|
|
621
|
+
[],
|
|
622
|
+
"Routine hook mint",
|
|
623
|
+
);
|
|
624
|
+
if (candidate.schemaVersion !== 1) {
|
|
625
|
+
throw new RoutineDecodeError(
|
|
626
|
+
"Routine hook mint schemaVersion is unsupported",
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
schemaVersion: 1,
|
|
631
|
+
routineId: commandIdentifier(candidate.routineId, "Routine id"),
|
|
632
|
+
keyVersion: hookKeyVersion(candidate.keyVersion),
|
|
633
|
+
token: routineText(candidate.token, 2_048, "Routine hook token"),
|
|
634
|
+
path: routineText(candidate.path, 512, "Routine hook path"),
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function decodeRoutineCommandReceiptV1(
|
|
639
|
+
value: unknown,
|
|
640
|
+
): RoutineCommandReceiptV1 {
|
|
641
|
+
const candidate = record(value, "Routine command receipt");
|
|
642
|
+
if (candidate.schemaVersion !== 1) {
|
|
643
|
+
throw new RoutineDecodeError(
|
|
644
|
+
"Routine command receipt schemaVersion is unsupported",
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
if (candidate.status === "deleted") {
|
|
648
|
+
routineExactKeys(
|
|
649
|
+
candidate,
|
|
650
|
+
["schemaVersion", "commandId", "status", "routineId"],
|
|
651
|
+
[],
|
|
652
|
+
"Routine command receipt",
|
|
653
|
+
);
|
|
654
|
+
return {
|
|
655
|
+
schemaVersion: 1,
|
|
656
|
+
commandId: commandIdentifier(candidate.commandId, "Routine commandId"),
|
|
657
|
+
status: "deleted",
|
|
658
|
+
routineId: commandIdentifier(candidate.routineId, "Routine id"),
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (candidate.status === "fired") {
|
|
662
|
+
routineExactKeys(
|
|
663
|
+
candidate,
|
|
664
|
+
["schemaVersion", "commandId", "status", "routineId", "fireId"],
|
|
665
|
+
[],
|
|
666
|
+
"Routine command receipt",
|
|
667
|
+
);
|
|
668
|
+
return {
|
|
669
|
+
schemaVersion: 1,
|
|
670
|
+
commandId: commandIdentifier(candidate.commandId, "Routine commandId"),
|
|
671
|
+
status: "fired",
|
|
672
|
+
routineId: commandIdentifier(candidate.routineId, "Routine id"),
|
|
673
|
+
fireId: routineText(candidate.fireId, 256, "Routine fireId"),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
if (candidate.status !== "applied") {
|
|
677
|
+
throw new RoutineDecodeError("Routine command receipt status is invalid");
|
|
678
|
+
}
|
|
679
|
+
routineExactKeys(
|
|
680
|
+
candidate,
|
|
681
|
+
["schemaVersion", "commandId", "status", "routine"],
|
|
682
|
+
["hook"],
|
|
683
|
+
"Routine command receipt",
|
|
684
|
+
);
|
|
685
|
+
return {
|
|
686
|
+
schemaVersion: 1,
|
|
687
|
+
commandId: commandIdentifier(candidate.commandId, "Routine commandId"),
|
|
688
|
+
status: "applied",
|
|
689
|
+
routine: decodeRoutineViewV1(candidate.routine),
|
|
690
|
+
...(candidate.hook === undefined
|
|
691
|
+
? {}
|
|
692
|
+
: { hook: decodeRoutineHookMintV1(candidate.hook) }),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** Most inbox entries one listing carries. */
|
|
697
|
+
export const ROUTINE_INBOX_LIST_MAX = 100;
|
|
698
|
+
/** Most events one automation run's read-only view carries. */
|
|
699
|
+
export const ROUTINE_RUN_EVENT_MAX = 200;
|
|
700
|
+
|
|
701
|
+
/** One completed automation Turn, as the hosted client sees it. */
|
|
702
|
+
export interface RoutineInboxEntryViewV1 {
|
|
703
|
+
schemaVersion: 1;
|
|
704
|
+
entryId: string;
|
|
705
|
+
runId: string;
|
|
706
|
+
routineId: string;
|
|
707
|
+
text: string;
|
|
708
|
+
attribution: string;
|
|
709
|
+
createdAt: string;
|
|
710
|
+
acknowledged: boolean;
|
|
711
|
+
acknowledgedAt?: string;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export interface RoutineInboxViewV1 {
|
|
715
|
+
schemaVersion: 1;
|
|
716
|
+
botId: string;
|
|
717
|
+
entries: RoutineInboxEntryViewV1[];
|
|
718
|
+
/** What the header badge shows, so the client counts nothing itself. */
|
|
719
|
+
unacknowledged: number;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Acknowledging is a command, never a side effect of reading: a background poll
|
|
724
|
+
* must not clear a badge. An empty `entryIds` acknowledges every entry.
|
|
725
|
+
*/
|
|
726
|
+
export interface RoutineInboxCommandV1 {
|
|
727
|
+
schemaVersion: 1;
|
|
728
|
+
commandId: string;
|
|
729
|
+
botId: string;
|
|
730
|
+
type: "routine/acknowledge-inbox";
|
|
731
|
+
entryIds: string[];
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export interface RoutineInboxReceiptV1 {
|
|
735
|
+
schemaVersion: 1;
|
|
736
|
+
commandId: string;
|
|
737
|
+
status: "applied";
|
|
738
|
+
inbox: RoutineInboxViewV1;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* One event of an automation run, flattened for the run-log reader.
|
|
743
|
+
*
|
|
744
|
+
* An automation run is absent from the visible transcript by construction, so
|
|
745
|
+
* the only way to read one is here — and it is a read, in a shape that carries
|
|
746
|
+
* no tool inputs, no model requests and no payloads, only what happened.
|
|
747
|
+
*/
|
|
748
|
+
export interface RoutineRunEventViewV1 {
|
|
749
|
+
type: string;
|
|
750
|
+
at: string;
|
|
751
|
+
summary: string;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** One automation run, read-only, reachable only through the run log. */
|
|
755
|
+
export interface RoutineRunDetailViewV1 {
|
|
756
|
+
schemaVersion: 1;
|
|
757
|
+
botId: string;
|
|
758
|
+
routineId: string;
|
|
759
|
+
runId: string;
|
|
760
|
+
status: string;
|
|
761
|
+
admittedAt: string;
|
|
762
|
+
input: string;
|
|
763
|
+
events: RoutineRunEventViewV1[];
|
|
764
|
+
outcome?: string;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export function decodeRoutineInboxEntryViewV1(
|
|
768
|
+
value: unknown,
|
|
769
|
+
): RoutineInboxEntryViewV1 {
|
|
770
|
+
const candidate = record(value, "Routine inbox entry view");
|
|
771
|
+
routineExactKeys(
|
|
772
|
+
candidate,
|
|
773
|
+
[
|
|
774
|
+
"schemaVersion",
|
|
775
|
+
"entryId",
|
|
776
|
+
"runId",
|
|
777
|
+
"routineId",
|
|
778
|
+
"text",
|
|
779
|
+
"attribution",
|
|
780
|
+
"createdAt",
|
|
781
|
+
"acknowledged",
|
|
782
|
+
],
|
|
783
|
+
["acknowledgedAt"],
|
|
784
|
+
"Routine inbox entry view",
|
|
785
|
+
);
|
|
786
|
+
if (candidate.schemaVersion !== 1) {
|
|
787
|
+
throw new RoutineDecodeError(
|
|
788
|
+
"Routine inbox entry view schemaVersion is unsupported",
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
if (typeof candidate.acknowledged !== "boolean") {
|
|
792
|
+
throw new RoutineDecodeError(
|
|
793
|
+
"Routine inbox entry view acknowledged must be a boolean",
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
schemaVersion: 1,
|
|
798
|
+
entryId: routineText(candidate.entryId, 256, "Routine inbox entryId"),
|
|
799
|
+
runId: routineText(candidate.runId, 256, "Routine inbox runId"),
|
|
800
|
+
routineId: commandIdentifier(
|
|
801
|
+
candidate.routineId,
|
|
802
|
+
"Routine inbox routineId",
|
|
803
|
+
),
|
|
804
|
+
text: routineText(candidate.text, 4_000, "Routine inbox text"),
|
|
805
|
+
attribution: routineText(
|
|
806
|
+
candidate.attribution,
|
|
807
|
+
128,
|
|
808
|
+
"Routine inbox attribution",
|
|
809
|
+
),
|
|
810
|
+
createdAt: routineTimestamp(candidate.createdAt, "Routine inbox createdAt"),
|
|
811
|
+
acknowledged: candidate.acknowledged,
|
|
812
|
+
...(candidate.acknowledgedAt === undefined
|
|
813
|
+
? {}
|
|
814
|
+
: {
|
|
815
|
+
acknowledgedAt: routineTimestamp(
|
|
816
|
+
candidate.acknowledgedAt,
|
|
817
|
+
"Routine inbox acknowledgedAt",
|
|
818
|
+
),
|
|
819
|
+
}),
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export function decodeRoutineInboxViewV1(value: unknown): RoutineInboxViewV1 {
|
|
824
|
+
const candidate = record(value, "Routine inbox view");
|
|
825
|
+
routineExactKeys(
|
|
826
|
+
candidate,
|
|
827
|
+
["schemaVersion", "botId", "entries", "unacknowledged"],
|
|
828
|
+
[],
|
|
829
|
+
"Routine inbox view",
|
|
830
|
+
);
|
|
831
|
+
if (candidate.schemaVersion !== 1) {
|
|
832
|
+
throw new RoutineDecodeError(
|
|
833
|
+
"Routine inbox view schemaVersion is unsupported",
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
if (!Array.isArray(candidate.entries)) {
|
|
837
|
+
throw new RoutineDecodeError("Routine inbox view entries must be an array");
|
|
838
|
+
}
|
|
839
|
+
if (candidate.entries.length > ROUTINE_INBOX_LIST_MAX) {
|
|
840
|
+
throw new RoutineDecodeError("Routine inbox view is over its bound");
|
|
841
|
+
}
|
|
842
|
+
if (
|
|
843
|
+
!Number.isSafeInteger(candidate.unacknowledged) ||
|
|
844
|
+
(candidate.unacknowledged as number) < 0
|
|
845
|
+
) {
|
|
846
|
+
throw new RoutineDecodeError(
|
|
847
|
+
"Routine inbox view unacknowledged must be a non-negative integer",
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
schemaVersion: 1,
|
|
852
|
+
botId: commandIdentifier(candidate.botId, "Routine inbox view botId"),
|
|
853
|
+
entries: candidate.entries.map(decodeRoutineInboxEntryViewV1),
|
|
854
|
+
unacknowledged: candidate.unacknowledged as number,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
export function decodeRoutineInboxCommandV1(
|
|
859
|
+
value: unknown,
|
|
860
|
+
): RoutineInboxCommandV1 {
|
|
861
|
+
const candidate = record(value, "Routine inbox command");
|
|
862
|
+
routineExactKeys(
|
|
863
|
+
candidate,
|
|
864
|
+
["schemaVersion", "commandId", "botId", "type", "entryIds"],
|
|
865
|
+
[],
|
|
866
|
+
"Routine inbox command",
|
|
867
|
+
);
|
|
868
|
+
if (candidate.schemaVersion !== 1) {
|
|
869
|
+
throw new RoutineDecodeError(
|
|
870
|
+
"Routine inbox command schemaVersion is unsupported",
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
if (candidate.type !== "routine/acknowledge-inbox") {
|
|
874
|
+
throw new RoutineDecodeError("Routine inbox command type is invalid");
|
|
875
|
+
}
|
|
876
|
+
if (!Array.isArray(candidate.entryIds)) {
|
|
877
|
+
throw new RoutineDecodeError(
|
|
878
|
+
"Routine inbox command entryIds must be an array",
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (candidate.entryIds.length > ROUTINE_INBOX_LIST_MAX) {
|
|
882
|
+
throw new RoutineDecodeError("Routine inbox command is over its bound");
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
schemaVersion: 1,
|
|
886
|
+
commandId: commandIdentifier(candidate.commandId, "Routine commandId"),
|
|
887
|
+
botId: commandIdentifier(candidate.botId, "Routine inbox command botId"),
|
|
888
|
+
type: "routine/acknowledge-inbox",
|
|
889
|
+
entryIds: candidate.entryIds.map((entryId) =>
|
|
890
|
+
routineText(entryId, 256, "Routine inbox command entryId"),
|
|
891
|
+
),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
export function decodeRoutineInboxReceiptV1(
|
|
896
|
+
value: unknown,
|
|
897
|
+
): RoutineInboxReceiptV1 {
|
|
898
|
+
const candidate = record(value, "Routine inbox receipt");
|
|
899
|
+
routineExactKeys(
|
|
900
|
+
candidate,
|
|
901
|
+
["schemaVersion", "commandId", "status", "inbox"],
|
|
902
|
+
[],
|
|
903
|
+
"Routine inbox receipt",
|
|
904
|
+
);
|
|
905
|
+
if (candidate.schemaVersion !== 1 || candidate.status !== "applied") {
|
|
906
|
+
throw new RoutineDecodeError("Routine inbox receipt is invalid");
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
schemaVersion: 1,
|
|
910
|
+
commandId: commandIdentifier(candidate.commandId, "Routine commandId"),
|
|
911
|
+
status: "applied",
|
|
912
|
+
inbox: decodeRoutineInboxViewV1(candidate.inbox),
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
export function decodeRoutineRunDetailViewV1(
|
|
917
|
+
value: unknown,
|
|
918
|
+
): RoutineRunDetailViewV1 {
|
|
919
|
+
const candidate = record(value, "Routine run detail");
|
|
920
|
+
routineExactKeys(
|
|
921
|
+
candidate,
|
|
922
|
+
[
|
|
923
|
+
"schemaVersion",
|
|
924
|
+
"botId",
|
|
925
|
+
"routineId",
|
|
926
|
+
"runId",
|
|
927
|
+
"status",
|
|
928
|
+
"admittedAt",
|
|
929
|
+
"input",
|
|
930
|
+
"events",
|
|
931
|
+
],
|
|
932
|
+
["outcome"],
|
|
933
|
+
"Routine run detail",
|
|
934
|
+
);
|
|
935
|
+
if (candidate.schemaVersion !== 1) {
|
|
936
|
+
throw new RoutineDecodeError(
|
|
937
|
+
"Routine run detail schemaVersion is unsupported",
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
if (!Array.isArray(candidate.events)) {
|
|
941
|
+
throw new RoutineDecodeError("Routine run detail events must be an array");
|
|
942
|
+
}
|
|
943
|
+
if (candidate.events.length > ROUTINE_RUN_EVENT_MAX) {
|
|
944
|
+
throw new RoutineDecodeError("Routine run detail is over its bound");
|
|
945
|
+
}
|
|
946
|
+
return {
|
|
947
|
+
schemaVersion: 1,
|
|
948
|
+
botId: commandIdentifier(candidate.botId, "Routine run detail botId"),
|
|
949
|
+
routineId: commandIdentifier(
|
|
950
|
+
candidate.routineId,
|
|
951
|
+
"Routine run detail routineId",
|
|
952
|
+
),
|
|
953
|
+
runId: routineText(candidate.runId, 256, "Routine run detail runId"),
|
|
954
|
+
status: routineText(candidate.status, 64, "Routine run detail status"),
|
|
955
|
+
admittedAt: routineTimestamp(
|
|
956
|
+
candidate.admittedAt,
|
|
957
|
+
"Routine run detail admittedAt",
|
|
958
|
+
),
|
|
959
|
+
input: routineText(candidate.input, 16_000, "Routine run detail input"),
|
|
960
|
+
events: candidate.events.map((event) => {
|
|
961
|
+
const entry = record(event, "Routine run detail event");
|
|
962
|
+
routineExactKeys(
|
|
963
|
+
entry,
|
|
964
|
+
["type", "at", "summary"],
|
|
965
|
+
[],
|
|
966
|
+
"Routine run detail event",
|
|
967
|
+
);
|
|
968
|
+
return {
|
|
969
|
+
type: routineText(entry.type, 64, "Routine run detail event type"),
|
|
970
|
+
at: routineTimestamp(entry.at, "Routine run detail event at"),
|
|
971
|
+
summary: routineText(
|
|
972
|
+
entry.summary,
|
|
973
|
+
2_000,
|
|
974
|
+
"Routine run detail event summary",
|
|
975
|
+
),
|
|
976
|
+
};
|
|
977
|
+
}),
|
|
978
|
+
...(candidate.outcome === undefined
|
|
979
|
+
? {}
|
|
980
|
+
: {
|
|
981
|
+
outcome: routineText(
|
|
982
|
+
candidate.outcome,
|
|
983
|
+
4_000,
|
|
984
|
+
"Routine run detail outcome",
|
|
985
|
+
),
|
|
986
|
+
}),
|
|
987
|
+
};
|
|
988
|
+
}
|