@frockbot/plugin-shell 0.0.0 → 0.1.1

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.
Files changed (73) hide show
  1. package/frockbot.json +68 -0
  2. package/package.json +87 -6
  3. package/src/agent.test.ts +372 -0
  4. package/src/agent.ts +335 -0
  5. package/src/approvals.test.ts +224 -0
  6. package/src/approvals.ts +530 -0
  7. package/src/backend-assignment.test.ts +161 -0
  8. package/src/backend-assignment.ts +274 -0
  9. package/src/backend-authoring.test.ts +518 -0
  10. package/src/backend-authoring.ts +531 -0
  11. package/src/backend-bot-identity.test.ts +215 -0
  12. package/src/backend-completion.test.ts +289 -0
  13. package/src/backend-completion.ts +95 -0
  14. package/src/backend-composition.ts +242 -0
  15. package/src/backend-computer.ts +76 -0
  16. package/src/backend-configuration.test.ts +1757 -0
  17. package/src/backend-contracts.test.ts +189 -0
  18. package/src/backend-contracts.ts +44 -0
  19. package/src/backend-debug.test.ts +202 -0
  20. package/src/backend-execution.ts +55 -0
  21. package/src/backend-flock.ts +96 -0
  22. package/src/backend-image.test.ts +115 -0
  23. package/src/backend-image.ts +180 -0
  24. package/src/backend-isolate.test.ts +238 -0
  25. package/src/backend-isolate.ts +409 -0
  26. package/src/backend-machine.ts +144 -0
  27. package/src/backend-memory.ts +89 -0
  28. package/src/backend-recovery-integration.test.ts +1575 -0
  29. package/src/backend-recovery.ts +106 -0
  30. package/src/backend-routines.ts +375 -0
  31. package/src/backend-runner.ts +251 -0
  32. package/src/backend-skills.test.ts +126 -0
  33. package/src/backend-skills.ts +198 -0
  34. package/src/backend-stop.test.ts +356 -0
  35. package/src/backend-subagents.ts +459 -0
  36. package/src/backend.ts +6035 -0
  37. package/src/client/FrockBotApp.vue +1026 -0
  38. package/src/client/SendPayloadView.vue +337 -0
  39. package/src/client/composer-draft.test.ts +31 -0
  40. package/src/client/composer-draft.ts +35 -0
  41. package/src/client/cordis-client-shim.d.ts +15 -0
  42. package/src/client/index.test.ts +2548 -0
  43. package/src/client/index.ts +2346 -0
  44. package/src/client/model-presentation.test.ts +35 -0
  45. package/src/client/model-presentation.ts +19 -0
  46. package/src/client/notify.test.ts +89 -0
  47. package/src/client/notify.ts +101 -0
  48. package/src/client/skill-invocation.test.ts +143 -0
  49. package/src/client/skill-invocation.ts +175 -0
  50. package/src/client/styles.css +1043 -0
  51. package/src/composition-views.ts +118 -0
  52. package/src/debug-protocol.test.ts +80 -0
  53. package/src/debug-protocol.ts +165 -0
  54. package/src/env.d.ts +10 -0
  55. package/src/history.test.ts +163 -0
  56. package/src/history.ts +108 -0
  57. package/src/host.ts +20 -0
  58. package/src/index.ts +2 -0
  59. package/src/manifest.ts +3 -0
  60. package/src/run-cursor.ts +28 -0
  61. package/src/run-protocol.test.ts +1281 -0
  62. package/src/run-protocol.ts +1417 -0
  63. package/src/settings-links.test.ts +106 -0
  64. package/src/settings-links.ts +289 -0
  65. package/src/shared.ts +338 -0
  66. package/src/skill-protocol.ts +117 -0
  67. package/src/terminal-records.test.ts +217 -0
  68. package/src/terminal-records.ts +150 -0
  69. package/src/unread.test.ts +362 -0
  70. package/src/unread.ts +675 -0
  71. package/tsconfig.json +18 -0
  72. package/vite.config.ts +32 -0
  73. package/README.md +0 -3
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Everything the Shell writes in the transaction that settles a Turn.
3
+ *
4
+ * Three policies share the kernel's one `terminalRecords` seam and none of them
5
+ * knows about the others: unread state advances for a conversational Turn, an
6
+ * automation Turn writes its completion-inbox entry and the pending input it
7
+ * hands off, and any Turn that asked for an approval writes the durable pending
8
+ * decision. The kernel writes the returned keys without reading them, so this
9
+ * is the only place their composition is decided.
10
+ *
11
+ * Two rules the composition itself has to keep, and they are why this is a
12
+ * function with a test rather than three spreads in a method body.
13
+ *
14
+ * * **Each producer runs exactly once per settlement.** A settlement that ran
15
+ * a producer twice would write two records where the Turn earned one — a
16
+ * firing with two inbox entries, say — and the second would be invisible
17
+ * until somebody counted.
18
+ *
19
+ * * **No producer may silently overwrite another.** A spread lets a later key
20
+ * clobber an earlier one with no sign; a collision here is a bug in the key
21
+ * spaces two Packages chose, so it throws rather than picking a winner.
22
+ *
23
+ * One settlement also gets one `now`. Three producers each reading their own
24
+ * clock would stamp one transaction with three different instants.
25
+ */
26
+ import {
27
+ advanceUnreadActivityV1,
28
+ optionalUnreadStateV1,
29
+ sidebarMessagePreviewForTurnV1,
30
+ SIDEBAR_PREVIEW_KEY,
31
+ UNREAD_STATE_KEY,
32
+ } from "./unread.js";
33
+ import { approvalTerminalRecordsV1 } from "./approvals.js";
34
+ import { routineTerminalRecordsForRunV1 } from "./backend-routines.js";
35
+
36
+ /** The settled run a terminal record set is computed from. */
37
+ export interface ShellTerminalRunV1 {
38
+ runId: string;
39
+ sessionId: string;
40
+ acceptedAt: string;
41
+ input: string;
42
+ events: readonly { type: string }[];
43
+ responseText?: string;
44
+ admission?: {
45
+ turnType?: string;
46
+ // Wide on purpose: the kernel records one origin shape per producer, and
47
+ // this module only asks which producer it was.
48
+ origin?: { kind: string; routineId?: string };
49
+ };
50
+ }
51
+
52
+ export interface ShellTerminalInputV1 {
53
+ run: ShellTerminalRunV1;
54
+ /** The admission-index cursor the Turn was admitted under. */
55
+ cursor: string;
56
+ /** The one instant this settlement is stamped with. */
57
+ now: string;
58
+ /** Reader bound to the transaction that is settling the Turn. */
59
+ read<T>(key: string): Promise<T | undefined>;
60
+ }
61
+
62
+ /**
63
+ * The unread record — FrockBot's `lastTurnSettlement`. Activity advances
64
+ * whatever the Bot's notification policy says: muting silences the intent,
65
+ * never the badge. Only a chat Turn advances it; an automation Turn reaches
66
+ * the User through its own inbox entry.
67
+ */
68
+ async function unreadRecordsV1(
69
+ input: ShellTerminalInputV1,
70
+ ): Promise<Record<string, unknown>> {
71
+ if ((input.run.admission?.turnType ?? "chat") !== "chat") return {};
72
+ const current = optionalUnreadStateV1(
73
+ await input.read<unknown>(UNREAD_STATE_KEY),
74
+ );
75
+ const next = advanceUnreadActivityV1(current, {
76
+ cursor: input.cursor,
77
+ at: input.now,
78
+ });
79
+ const preview = sidebarMessagePreviewForTurnV1(input.run, input.now);
80
+ return {
81
+ [UNREAD_STATE_KEY]: next,
82
+ // `advanceUnreadActivityV1` returns the current object for a replay or an
83
+ // older settlement. The preview follows that same monotonic decision, so
84
+ // recovery cannot replace a newer row with an older Turn.
85
+ ...(next === current || preview === undefined
86
+ ? {}
87
+ : { [SIDEBAR_PREVIEW_KEY]: preview }),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * The completion-inbox entry and pending wake an automation Turn contributes.
93
+ * Nothing for a conversational one, so a chat Turn's settlement is
94
+ * byte-for-byte what it was before Routines existed.
95
+ */
96
+ async function routineRecordsV1(
97
+ input: ShellTerminalInputV1,
98
+ ): Promise<Record<string, unknown>> {
99
+ const contributed = await routineTerminalRecordsForRunV1({
100
+ run: input.run,
101
+ read: input.read,
102
+ now: input.now,
103
+ });
104
+ return contributed?.records ?? {};
105
+ }
106
+
107
+ /**
108
+ * The pending decisions the Turn asked for. "A request for more becomes a
109
+ * durable pending decision for the User, never a grant": the card the User
110
+ * sees and the record their answer is written against become durable in one
111
+ * transaction, so there is no instant at which the question has been asked and
112
+ * the answer has nowhere to go.
113
+ */
114
+ async function approvalRecordsV1(
115
+ input: ShellTerminalInputV1,
116
+ ): Promise<Record<string, unknown>> {
117
+ return approvalTerminalRecordsV1({
118
+ run: {
119
+ runId: input.run.runId,
120
+ sessionId: input.run.sessionId,
121
+ events: input.run.events,
122
+ },
123
+ now: input.now,
124
+ read: input.read,
125
+ });
126
+ }
127
+
128
+ /** The producers, in the order they are composed. Each is called once. */
129
+ const SHELL_TERMINAL_PRODUCERS_V1 = [
130
+ unreadRecordsV1,
131
+ routineRecordsV1,
132
+ approvalRecordsV1,
133
+ ] as const;
134
+
135
+ export async function shellTerminalRecordsV1(
136
+ input: ShellTerminalInputV1,
137
+ ): Promise<Record<string, unknown>> {
138
+ const records: Record<string, unknown> = {};
139
+ for (const produce of SHELL_TERMINAL_PRODUCERS_V1) {
140
+ for (const [key, value] of Object.entries(await produce(input))) {
141
+ if (Object.hasOwn(records, key)) {
142
+ throw new Error(
143
+ `two terminal-record producers both wrote "${key}"; one would have silently overwritten the other`,
144
+ );
145
+ }
146
+ records[key] = value;
147
+ }
148
+ }
149
+ return records;
150
+ }
@@ -0,0 +1,362 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ advanceUnreadActivityV1,
4
+ botUnreadCommandFingerprintV1,
5
+ decodeSidebarMessagePreviewV1,
6
+ decodeBotNotificationDirectoryViewV1,
7
+ decodeBotUnreadCommandV1,
8
+ decodeBotUnreadDirectoryViewV1,
9
+ decodeUnreadStateV1,
10
+ emptyUnreadStateV1,
11
+ markUnreadReadV1,
12
+ markUnreadV1,
13
+ optionalUnreadStateV1,
14
+ projectBotUnreadViewV1,
15
+ sidebarMessagePreviewForTurnV1,
16
+ UNREAD_COUNT_CAP,
17
+ type UnreadStateV1,
18
+ } from "./unread.js";
19
+
20
+ /** A cursor in the exact shape the Bot's admission index writes. */
21
+ function cursor(minute: number, runId = `run-${minute}`): string {
22
+ const at = new Date(Date.UTC(2026, 7, 31, 0, minute, 0)).toISOString();
23
+ return `run-index:${at}:${runId}`;
24
+ }
25
+
26
+ /** Newest-first, exactly as `listRunIndex` returns it. */
27
+ function index(count: number): string[] {
28
+ return Array.from({ length: count }, (_, offset) => cursor(count - offset));
29
+ }
30
+
31
+ describe("the unread codec", () => {
32
+ test("accepts the empty record and a full one", () => {
33
+ expect(decodeUnreadStateV1(emptyUnreadStateV1())).toEqual({
34
+ schemaVersion: 1,
35
+ manuallyUnread: false,
36
+ });
37
+ const full: UnreadStateV1 = {
38
+ schemaVersion: 1,
39
+ lastActivityCursor: cursor(2),
40
+ lastActivityAt: "2026-08-31T00:02:00.000Z",
41
+ lastSeenCursor: cursor(1),
42
+ lastViewedAt: "2026-08-31T00:01:30.000Z",
43
+ manuallyUnread: true,
44
+ };
45
+ expect(decodeUnreadStateV1(full)).toEqual(full);
46
+ });
47
+
48
+ test("refuses anything that is not the record it claims to be", () => {
49
+ const base = emptyUnreadStateV1();
50
+ for (const invalid of [
51
+ undefined,
52
+ null,
53
+ [],
54
+ "unread",
55
+ { ...base, schemaVersion: 2 },
56
+ { ...base, manuallyUnread: "yes" },
57
+ { ...base, surprise: true },
58
+ { schemaVersion: 1 },
59
+ // A cursor that is not an admission-index key.
60
+ { ...base, lastSeenCursor: "yesterday", lastViewedAt: base.lastViewedAt },
61
+ { ...base, lastSeenCursor: cursor(1) },
62
+ { ...base, lastActivityAt: "2026-08-31T00:01:00.000Z" },
63
+ {
64
+ ...base,
65
+ lastActivityCursor: cursor(1),
66
+ lastActivityAt: "not-a-time",
67
+ },
68
+ ]) {
69
+ expect(() => decodeUnreadStateV1(invalid)).toThrow();
70
+ }
71
+ });
72
+
73
+ test("reads an absent record as the empty one", () => {
74
+ expect(optionalUnreadStateV1(undefined)).toEqual(emptyUnreadStateV1());
75
+ expect(() => optionalUnreadStateV1({ schemaVersion: 3 })).toThrow();
76
+ });
77
+ });
78
+
79
+ describe("the sidebar message preview", () => {
80
+ const preview = {
81
+ schemaVersion: 1 as const,
82
+ text: "Done",
83
+ at: "2026-08-31T00:02:00.000Z",
84
+ role: "assistant" as const,
85
+ };
86
+
87
+ test("decodes the exact bounded projection", () => {
88
+ expect(decodeSidebarMessagePreviewV1(preview)).toEqual(preview);
89
+ for (const invalid of [
90
+ { ...preview, extra: true },
91
+ { ...preview, text: "" },
92
+ { ...preview, text: "x".repeat(121) },
93
+ { ...preview, at: "yesterday" },
94
+ { ...preview, role: "system" },
95
+ ]) {
96
+ expect(() => decodeSidebarMessagePreviewV1(invalid)).toThrow();
97
+ }
98
+ });
99
+
100
+ test("prefers assistant text, falls back to the User, and bounds both", () => {
101
+ expect(
102
+ sidebarMessagePreviewForTurnV1(
103
+ {
104
+ acceptedAt: "2026-08-31T00:01:00.000Z",
105
+ input: "Question",
106
+ responseText: "Answer",
107
+ },
108
+ preview.at,
109
+ ),
110
+ ).toMatchObject({ text: "Answer", at: preview.at, role: "assistant" });
111
+ expect(
112
+ sidebarMessagePreviewForTurnV1(
113
+ {
114
+ acceptedAt: "2026-08-31T00:01:00.000Z",
115
+ input: "Q".repeat(140),
116
+ responseText: "",
117
+ },
118
+ preview.at,
119
+ ),
120
+ ).toMatchObject({
121
+ text: "Q".repeat(120),
122
+ at: "2026-08-31T00:01:00.000Z",
123
+ role: "user",
124
+ });
125
+ });
126
+ });
127
+
128
+ describe("unread transitions", () => {
129
+ test("activity is monotonic, so a re-settled Turn cannot move it", () => {
130
+ const first = advanceUnreadActivityV1(emptyUnreadStateV1(), {
131
+ cursor: cursor(2),
132
+ at: "2026-08-31T00:02:00.000Z",
133
+ });
134
+ // Recovery settling the same Turn again, and an older Turn arriving late.
135
+ const replayed = advanceUnreadActivityV1(first, {
136
+ cursor: cursor(2),
137
+ at: "2026-08-31T00:09:00.000Z",
138
+ });
139
+ expect(replayed).toEqual(first);
140
+ expect(
141
+ advanceUnreadActivityV1(first, {
142
+ cursor: cursor(1),
143
+ at: "2026-08-31T00:01:00.000Z",
144
+ }),
145
+ ).toEqual(first);
146
+ const later = advanceUnreadActivityV1(first, {
147
+ cursor: cursor(3),
148
+ at: "2026-08-31T00:03:00.000Z",
149
+ });
150
+ expect(later.lastActivityCursor).toBe(cursor(3));
151
+ });
152
+
153
+ test("mark-read takes the max, so out-of-order marks are safe", () => {
154
+ const read = markUnreadReadV1(emptyUnreadStateV1(), {
155
+ upToCursor: cursor(5),
156
+ at: "2026-08-31T00:05:00.000Z",
157
+ });
158
+ const stale = markUnreadReadV1(read, {
159
+ upToCursor: cursor(2),
160
+ at: "2026-08-31T00:02:00.000Z",
161
+ });
162
+ expect(stale.lastSeenCursor).toBe(cursor(5));
163
+ expect(stale.lastViewedAt).toBe("2026-08-31T00:05:00.000Z");
164
+ const newer = markUnreadReadV1(stale, {
165
+ upToCursor: cursor(7),
166
+ at: "2026-08-31T00:07:00.000Z",
167
+ });
168
+ expect(newer.lastSeenCursor).toBe(cursor(7));
169
+ });
170
+
171
+ test("a later mark-read clears manual unread", () => {
172
+ const marked = markUnreadV1(
173
+ markUnreadReadV1(emptyUnreadStateV1(), {
174
+ upToCursor: cursor(1),
175
+ at: "2026-08-31T00:01:00.000Z",
176
+ }),
177
+ );
178
+ expect(marked.manuallyUnread).toBe(true);
179
+ // Even a mark that moves no cursor: the User is looking at the thread.
180
+ const read = markUnreadReadV1(marked, {
181
+ upToCursor: cursor(1),
182
+ at: "2026-08-31T00:04:00.000Z",
183
+ });
184
+ expect(read).toMatchObject({
185
+ manuallyUnread: false,
186
+ lastSeenCursor: cursor(1),
187
+ });
188
+ });
189
+ });
190
+
191
+ describe("the unread projection", () => {
192
+ test("counts only settled chat Turns the User has not seen", () => {
193
+ const state: UnreadStateV1 = {
194
+ schemaVersion: 1,
195
+ lastActivityCursor: cursor(3),
196
+ lastActivityAt: "2026-08-31T00:03:00.000Z",
197
+ lastSeenCursor: cursor(1),
198
+ lastViewedAt: "2026-08-31T00:01:00.000Z",
199
+ manuallyUnread: false,
200
+ };
201
+ // Four admitted Turns; the newest has not settled, so it is not counted.
202
+ const view = projectBotUnreadViewV1("alpha", state, index(4));
203
+ expect(view).toMatchObject({ botId: "alpha", count: 2, unread: true });
204
+ });
205
+
206
+ test("a manual mark shows unread with no count", () => {
207
+ const view = projectBotUnreadViewV1(
208
+ "alpha",
209
+ markUnreadV1(emptyUnreadStateV1()),
210
+ [],
211
+ );
212
+ expect(view).toMatchObject({
213
+ count: 0,
214
+ unread: true,
215
+ manuallyUnread: true,
216
+ });
217
+ });
218
+
219
+ test("caps the count and says so", () => {
220
+ const entries = index(UNREAD_COUNT_CAP + 1);
221
+ const state = advanceUnreadActivityV1(emptyUnreadStateV1(), {
222
+ cursor: entries[0]!,
223
+ at: "2026-08-31T02:00:00.000Z",
224
+ });
225
+ const view = projectBotUnreadViewV1("alpha", state, entries);
226
+ expect(view).toMatchObject({ count: UNREAD_COUNT_CAP, capped: true });
227
+ });
228
+
229
+ test("nothing settled means nothing unread", () => {
230
+ const view = projectBotUnreadViewV1(
231
+ "alpha",
232
+ emptyUnreadStateV1(),
233
+ index(3),
234
+ );
235
+ expect(view).toMatchObject({ count: 0, capped: false, unread: false });
236
+ });
237
+
238
+ test("carries the already-bounded latest message without deriving it", () => {
239
+ const preview = decodeSidebarMessagePreviewV1({
240
+ schemaVersion: 1,
241
+ text: "Latest answer",
242
+ at: "2026-08-31T00:03:00.000Z",
243
+ role: "assistant",
244
+ });
245
+ const projected = projectBotUnreadViewV1(
246
+ "alpha",
247
+ emptyUnreadStateV1(),
248
+ [],
249
+ preview,
250
+ );
251
+ expect(
252
+ decodeBotUnreadDirectoryViewV1({
253
+ schemaVersion: 1,
254
+ unread: [projected],
255
+ }).unread[0]?.lastMessage,
256
+ ).toEqual(preview);
257
+ expect(() =>
258
+ decodeBotUnreadDirectoryViewV1({
259
+ schemaVersion: 1,
260
+ unread: [{ ...projected, lastMessage: { ...preview, extra: true } }],
261
+ }),
262
+ ).toThrow();
263
+ });
264
+ });
265
+
266
+ describe("the unread command", () => {
267
+ test("decodes each type and refuses a mismatched cursor", () => {
268
+ expect(
269
+ decodeBotUnreadCommandV1({
270
+ schemaVersion: 1,
271
+ type: "bot/mark-read",
272
+ commandId: "mark-1",
273
+ botId: "alpha",
274
+ upToCursor: cursor(1),
275
+ }),
276
+ ).toMatchObject({ type: "bot/mark-read", upToCursor: cursor(1) });
277
+ expect(
278
+ decodeBotUnreadCommandV1({
279
+ schemaVersion: 1,
280
+ type: "bot/mark-unread",
281
+ commandId: "mark-2",
282
+ botId: "alpha",
283
+ }),
284
+ ).toMatchObject({ type: "bot/mark-unread" });
285
+ for (const invalid of [
286
+ // mark-read with no cursor, mark-unread with one.
287
+ {
288
+ schemaVersion: 1,
289
+ type: "bot/mark-read",
290
+ commandId: "mark-3",
291
+ botId: "alpha",
292
+ },
293
+ {
294
+ schemaVersion: 1,
295
+ type: "bot/mark-unread",
296
+ commandId: "mark-4",
297
+ botId: "alpha",
298
+ upToCursor: cursor(1),
299
+ },
300
+ {
301
+ schemaVersion: 1,
302
+ type: "bot/archive",
303
+ commandId: "mark-5",
304
+ botId: "alpha",
305
+ },
306
+ ]) {
307
+ expect(() => decodeBotUnreadCommandV1(invalid)).toThrow();
308
+ }
309
+ });
310
+
311
+ test("fingerprints the meaning, not the command id", () => {
312
+ const command = decodeBotUnreadCommandV1({
313
+ schemaVersion: 1,
314
+ type: "bot/mark-read",
315
+ commandId: "mark-1",
316
+ botId: "alpha",
317
+ upToCursor: cursor(1),
318
+ });
319
+ expect(botUnreadCommandFingerprintV1(command)).toBe(
320
+ botUnreadCommandFingerprintV1({ ...command, commandId: "mark-2" }),
321
+ );
322
+ expect(botUnreadCommandFingerprintV1(command)).not.toBe(
323
+ botUnreadCommandFingerprintV1({ ...command, upToCursor: cursor(2) }),
324
+ );
325
+ });
326
+ });
327
+
328
+ describe("the fan-out views", () => {
329
+ test("decode exactly what the routes answer", () => {
330
+ expect(
331
+ decodeBotUnreadDirectoryViewV1({
332
+ schemaVersion: 1,
333
+ unread: [projectBotUnreadViewV1("alpha", emptyUnreadStateV1(), [])],
334
+ }).unread,
335
+ ).toHaveLength(1);
336
+ expect(() =>
337
+ decodeBotUnreadDirectoryViewV1({ schemaVersion: 1, unread: [{}] }),
338
+ ).toThrow();
339
+ expect(
340
+ decodeBotNotificationDirectoryViewV1({
341
+ schemaVersion: 1,
342
+ notifications: [
343
+ {
344
+ schemaVersion: 1,
345
+ botId: "alpha",
346
+ notificationId: "run-1",
347
+ runId: "run-1",
348
+ createdAt: "2026-08-31T00:01:00.000Z",
349
+ title: "Alpha replied",
350
+ body: "hello",
351
+ },
352
+ ],
353
+ }).notifications,
354
+ ).toHaveLength(1);
355
+ expect(() =>
356
+ decodeBotNotificationDirectoryViewV1({
357
+ schemaVersion: 1,
358
+ notifications: [{ schemaVersion: 1, botId: "alpha" }],
359
+ }),
360
+ ).toThrow();
361
+ });
362
+ });