@frockbot/plugin-shell 0.3.12 → 0.3.14
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/package.json +34 -32
- package/src/agent.test.ts +78 -0
- package/src/agent.ts +65 -1
- package/src/backend-configuration.test.ts +20 -20
- package/src/backend-recovery-integration.test.ts +10 -10
- package/src/client/AppletCanvas.vue +19 -6
- package/src/client/FrockBotApp.vue +299 -68
- package/src/client/applets-client.test.ts +62 -0
- package/src/client/applets-client.ts +19 -0
- package/src/client/index.test.ts +103 -16
- package/src/client/index.ts +187 -79
- package/src/client/styles.css +56 -61
- package/src/client/voice-dictation.test.ts +105 -0
- package/src/client/voice-dictation.ts +137 -0
- package/src/client/voice-microphone.ts +125 -0
- package/src/client/voice-worklet.ts +75 -0
- package/src/notification-id.ts +0 -0
- package/src/run-failure-copy.test.ts +150 -0
- package/src/run-failure-copy.ts +110 -0
- package/src/run-protocol.test.ts +13 -7
- package/src/run-protocol.ts +17 -7
- package/src/shared.ts +17 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { SessionEvent } from "@frockbot/kernel-contracts";
|
|
3
|
+
import { MODEL_FIRST_BYTE_DEADLINE_REASON_V1 } from "@frockbot/kernel-contracts";
|
|
4
|
+
import {
|
|
5
|
+
knownFailureCopyV1,
|
|
6
|
+
RUN_FAILURE_COPY_V1,
|
|
7
|
+
RUN_FAILURE_FALLBACK_COPY_V1,
|
|
8
|
+
runFailureCopyV1,
|
|
9
|
+
USER_FACING_FAILURE_REASONS_V1,
|
|
10
|
+
} from "./run-failure-copy.js";
|
|
11
|
+
import { initializeBotSettingsV1 } from "@frockbot/configuration-core";
|
|
12
|
+
import type { StoredRun } from "./backend-contracts.js";
|
|
13
|
+
import { projectClientRunV1 } from "./run-protocol.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Words that describe the machine. Every one of them reached a chat bubble
|
|
17
|
+
* before this: the verification run read "Reconciliation was explicitly
|
|
18
|
+
* abandoned: Bot turn ended with outcome model-error: Flock AI keeps no durable
|
|
19
|
+
* copy of an interrupted response, so it cannot be recovered".
|
|
20
|
+
*/
|
|
21
|
+
const FORBIDDEN_V1 = [
|
|
22
|
+
"reconcil",
|
|
23
|
+
"outcome",
|
|
24
|
+
"durable",
|
|
25
|
+
"supersede",
|
|
26
|
+
"admission",
|
|
27
|
+
"provider",
|
|
28
|
+
"model-error",
|
|
29
|
+
"tool-error",
|
|
30
|
+
"turn/end",
|
|
31
|
+
"session event",
|
|
32
|
+
"run id",
|
|
33
|
+
"runid",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function assertPlainV1(copy: string): void {
|
|
37
|
+
const lowered = copy.toLowerCase();
|
|
38
|
+
for (const word of FORBIDDEN_V1) {
|
|
39
|
+
expect(lowered.includes(word)).toBe(false);
|
|
40
|
+
}
|
|
41
|
+
// A bare " run " is jargon; "running" and the like are not, so the check is
|
|
42
|
+
// on the word rather than the substring.
|
|
43
|
+
expect(/\brun(s|id)?\b/.test(lowered)).toBe(false);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const TIMESTAMP = "2026-09-04T00:00:00.000Z";
|
|
47
|
+
let seq = 0;
|
|
48
|
+
const turnEnd = (outcome: string) =>
|
|
49
|
+
({
|
|
50
|
+
type: "turn/end",
|
|
51
|
+
turn: 1,
|
|
52
|
+
outcome,
|
|
53
|
+
seq: (seq += 1),
|
|
54
|
+
timestamp: TIMESTAMP,
|
|
55
|
+
}) as unknown as SessionEvent;
|
|
56
|
+
|
|
57
|
+
function failedRun(failure: string, events: SessionEvent[]): StoredRun {
|
|
58
|
+
return {
|
|
59
|
+
runId: "run-1",
|
|
60
|
+
commandFingerprint: "fingerprint",
|
|
61
|
+
sessionId: "user:primary",
|
|
62
|
+
acceptedAt: TIMESTAMP,
|
|
63
|
+
input: "make me an applet",
|
|
64
|
+
events,
|
|
65
|
+
effectAdmissions: [],
|
|
66
|
+
status: "failed",
|
|
67
|
+
phase: "executing",
|
|
68
|
+
compositionGenerationId: "test-composition-generation",
|
|
69
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
70
|
+
previousEventCount: 0,
|
|
71
|
+
failure,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("runFailureCopyV1", () => {
|
|
76
|
+
test("every mapped sentence is written for a person", () => {
|
|
77
|
+
for (const copy of Object.values(RUN_FAILURE_COPY_V1)) assertPlainV1(copy);
|
|
78
|
+
assertPlainV1(RUN_FAILURE_FALLBACK_COPY_V1);
|
|
79
|
+
for (const reason of USER_FACING_FAILURE_REASONS_V1) assertPlainV1(reason);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("the stored diagnostic never reaches the copy", () => {
|
|
83
|
+
const failure =
|
|
84
|
+
"Reconciliation was explicitly abandoned: Bot turn ended with outcome model-error: Flock AI keeps no durable copy of an interrupted response, so it cannot be recovered";
|
|
85
|
+
const copy = runFailureCopyV1({
|
|
86
|
+
failure,
|
|
87
|
+
events: [turnEnd("interrupted")],
|
|
88
|
+
});
|
|
89
|
+
expect(copy).toBe(RUN_FAILURE_COPY_V1.interrupted);
|
|
90
|
+
assertPlainV1(copy);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a kernel sentence written for a person survives its wrapper", () => {
|
|
94
|
+
const copy = runFailureCopyV1({
|
|
95
|
+
failure: `Model request "abc" has no durable provider outcome: Model response outcome is uncertain: ${MODEL_FIRST_BYTE_DEADLINE_REASON_V1}`,
|
|
96
|
+
events: [turnEnd("interrupted")],
|
|
97
|
+
});
|
|
98
|
+
expect(copy).toBe(MODEL_FIRST_BYTE_DEADLINE_REASON_V1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a Turn with no terminal event still says something plain", () => {
|
|
102
|
+
expect(runFailureCopyV1({ failure: "boom" })).toBe(
|
|
103
|
+
RUN_FAILURE_FALLBACK_COPY_V1,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// The client's own guard, because a `ClientRun` can arrive from an older
|
|
108
|
+
// backend that forwarded the raw diagnostic, and the thread must not render
|
|
109
|
+
// a provider's words as though the Bot said them.
|
|
110
|
+
test("the thread accepts only sentences the product wrote", () => {
|
|
111
|
+
for (const written of [
|
|
112
|
+
...Object.values(RUN_FAILURE_COPY_V1),
|
|
113
|
+
...USER_FACING_FAILURE_REASONS_V1,
|
|
114
|
+
]) {
|
|
115
|
+
expect(knownFailureCopyV1(written)).toBe(written);
|
|
116
|
+
}
|
|
117
|
+
for (const diagnostic of [
|
|
118
|
+
undefined,
|
|
119
|
+
"",
|
|
120
|
+
"Provider reconciliation is required",
|
|
121
|
+
"Bot turn ended with outcome model-error: Model request failed (401)",
|
|
122
|
+
'Skill "bot/no-such-skill" is unknown',
|
|
123
|
+
]) {
|
|
124
|
+
const copy = knownFailureCopyV1(diagnostic);
|
|
125
|
+
expect(copy).toBe(RUN_FAILURE_FALLBACK_COPY_V1);
|
|
126
|
+
assertPlainV1(copy);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("the projection sends the copy, not the diagnostic", () => {
|
|
131
|
+
const projected = projectClientRunV1(
|
|
132
|
+
failedRun(
|
|
133
|
+
"Reconciliation was explicitly abandoned: Bot turn ended with outcome model-error",
|
|
134
|
+
[
|
|
135
|
+
{
|
|
136
|
+
type: "turn/start",
|
|
137
|
+
turn: 1,
|
|
138
|
+
seq: (seq += 1),
|
|
139
|
+
timestamp: TIMESTAMP,
|
|
140
|
+
} as unknown as SessionEvent,
|
|
141
|
+
turnEnd("interrupted"),
|
|
142
|
+
],
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
expect(projected.outcome?.type).toBe("failed");
|
|
146
|
+
if (projected.outcome?.type !== "failed") throw new Error("unreachable");
|
|
147
|
+
assertPlainV1(projected.outcome.message);
|
|
148
|
+
expect(projected.outcome.message).toBe(RUN_FAILURE_COPY_V1.interrupted);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { SessionEvent, TurnOutcome } from "@frockbot/kernel-contracts";
|
|
2
|
+
import {
|
|
3
|
+
MODEL_FIRST_BYTE_DEADLINE_REASON_V1,
|
|
4
|
+
MODEL_IDLE_DEADLINE_REASON_V1,
|
|
5
|
+
} from "@frockbot/kernel-contracts";
|
|
6
|
+
import { TURN_DEADLINE_REASON_V1 } from "@frockbot/kernel-agent-loop";
|
|
7
|
+
import { UNRECONCILABLE_RUN_FAILURE_V1 } from "@frockbot/kernel-do";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The one place a Turn that did not finish is turned into a sentence for the
|
|
11
|
+
* person who was waiting on it.
|
|
12
|
+
*
|
|
13
|
+
* A run's stored `failure` is a diagnostic. It is composed by whoever settled
|
|
14
|
+
* the run, out of whatever the layer below handed up, and it reads like it:
|
|
15
|
+
* "Reconciliation was explicitly abandoned: Bot turn ended with outcome
|
|
16
|
+
* model-error: Flock AI keeps no durable copy of an interrupted response, so it
|
|
17
|
+
* cannot be recovered". Every word of that is useful — on the debug surface,
|
|
18
|
+
* where it stays, unchanged. None of it belongs in a chat bubble. A person
|
|
19
|
+
* asked for a countdown applet; they should not have to learn what
|
|
20
|
+
* reconciliation is to find out that the model gave up.
|
|
21
|
+
*
|
|
22
|
+
* So the projection stops forwarding the diagnostic and picks the sentence
|
|
23
|
+
* instead. Two inputs, in order:
|
|
24
|
+
*
|
|
25
|
+
* 1. The kernel's own user-facing reasons. A handful of failures already have
|
|
26
|
+
* a sentence written for a person — the model deadlines, the Turn deadline,
|
|
27
|
+
* the unretrievable settlement — and those say something the outcome alone
|
|
28
|
+
* cannot, so a stored failure that carries one hands it straight through.
|
|
29
|
+
* 2. Otherwise the Turn's terminal outcome, which is a closed set, mapped
|
|
30
|
+
* below. It says less, and it can never leak.
|
|
31
|
+
*
|
|
32
|
+
* The diagnostic is never the answer, not even as a fallback: an unmapped
|
|
33
|
+
* outcome gets the generic line rather than whatever prose happened to be
|
|
34
|
+
* stored.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Sentences the kernel writes for the person rather than for the log. They are
|
|
39
|
+
* matched as substrings because the layer that settles a run wraps the reason
|
|
40
|
+
* it was handed — the sentence survives the wrapping, the wrapper does not.
|
|
41
|
+
*/
|
|
42
|
+
export const USER_FACING_FAILURE_REASONS_V1: readonly string[] = [
|
|
43
|
+
MODEL_FIRST_BYTE_DEADLINE_REASON_V1,
|
|
44
|
+
MODEL_IDLE_DEADLINE_REASON_V1,
|
|
45
|
+
TURN_DEADLINE_REASON_V1,
|
|
46
|
+
UNRECONCILABLE_RUN_FAILURE_V1,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What each terminal outcome says. Total over `TurnOutcome` so adding one to
|
|
51
|
+
* the kernel's union is a type error here rather than a silent generic line.
|
|
52
|
+
*/
|
|
53
|
+
export const RUN_FAILURE_COPY_V1: Record<TurnOutcome, string> = {
|
|
54
|
+
completed: "This Bot couldn't finish its reply. Try again.",
|
|
55
|
+
blocked: "This Bot wouldn't do that. Try asking a different way.",
|
|
56
|
+
cancelled: "You stopped this.",
|
|
57
|
+
interrupted: "This reply stopped before it finished. Try again.",
|
|
58
|
+
"model-error": "The model couldn't finish its reply. Try again.",
|
|
59
|
+
"tool-error": "Something the Bot was using didn't work. Try again.",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** What a Turn says when nothing more specific is known about how it ended. */
|
|
63
|
+
export const RUN_FAILURE_FALLBACK_COPY_V1 =
|
|
64
|
+
"This Bot couldn't finish its reply. Try again.";
|
|
65
|
+
|
|
66
|
+
/** The outcome the run's own log records, or `undefined` on an unclosed Turn. */
|
|
67
|
+
function terminalTurnOutcomeV1(
|
|
68
|
+
events: readonly SessionEvent[],
|
|
69
|
+
): TurnOutcome | undefined {
|
|
70
|
+
const terminal = events.findLast((event) => event.type === "turn/end");
|
|
71
|
+
return terminal?.type === "turn/end" ? terminal.outcome : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function runFailureCopyV1(input: {
|
|
75
|
+
failure?: string;
|
|
76
|
+
events?: readonly SessionEvent[];
|
|
77
|
+
}): string {
|
|
78
|
+
const failure = input.failure ?? "";
|
|
79
|
+
const written = USER_FACING_FAILURE_REASONS_V1.find((reason) =>
|
|
80
|
+
failure.includes(reason),
|
|
81
|
+
);
|
|
82
|
+
if (written) return written;
|
|
83
|
+
const outcome = terminalTurnOutcomeV1(input.events ?? []);
|
|
84
|
+
return outcome ? RUN_FAILURE_COPY_V1[outcome] : RUN_FAILURE_FALLBACK_COPY_V1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The product's own sentences, as a set a client can check a string against.
|
|
89
|
+
*
|
|
90
|
+
* The projection maps every failure through {@link runFailureCopyV1} on the way
|
|
91
|
+
* to the wire, so what reaches a client is already copy. The client still must
|
|
92
|
+
* not *trust* that: a `ClientRun` can arrive from an older backend that
|
|
93
|
+
* forwarded the raw diagnostic, and a provider's words under a bubble read as
|
|
94
|
+
* part of what the Bot was saying. So the thread accepts a failure only when it
|
|
95
|
+
* recognises it as something the product wrote, and otherwise says the generic
|
|
96
|
+
* line — which keeps the specific sentences (the model deadlines say something
|
|
97
|
+
* the outcome alone cannot) without ever letting an unknown string through.
|
|
98
|
+
*/
|
|
99
|
+
const KNOWN_FAILURE_COPY_V1 = new Set<string>([
|
|
100
|
+
...Object.values(RUN_FAILURE_COPY_V1),
|
|
101
|
+
...USER_FACING_FAILURE_REASONS_V1,
|
|
102
|
+
RUN_FAILURE_FALLBACK_COPY_V1,
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
/** The failure if the product wrote it, else the line every failure can use. */
|
|
106
|
+
export function knownFailureCopyV1(failure: string | undefined): string {
|
|
107
|
+
return failure && KNOWN_FAILURE_COPY_V1.has(failure)
|
|
108
|
+
? failure
|
|
109
|
+
: RUN_FAILURE_FALLBACK_COPY_V1;
|
|
110
|
+
}
|
package/src/run-protocol.test.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type SessionEvent } from "@frockbot/kernel-contracts";
|
|
|
3
3
|
import { initializeBotSettingsV1 } from "@frockbot/configuration-core";
|
|
4
4
|
import type { StoredRun } from "./backend-contracts.js";
|
|
5
5
|
import { planBotRunRecovery } from "./backend-recovery.js";
|
|
6
|
+
import { RUN_FAILURE_COPY_V1 } from "./run-failure-copy.js";
|
|
6
7
|
import {
|
|
7
8
|
createClientRunStopReceiptV1,
|
|
8
9
|
decodeClientNotificationAcknowledgementCommandV1,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
decodeClientRunLookupQueryV1,
|
|
11
12
|
decodeClientRunReconciliationCommandV1,
|
|
12
13
|
decodeClientRunStopCommandV1,
|
|
14
|
+
RESUMABLE_RUN_MESSAGE_V1,
|
|
13
15
|
decodeClientRunStopReceiptV1,
|
|
14
16
|
decodeClientTurnCommandV1,
|
|
15
17
|
decodeClientRunLookupV1,
|
|
@@ -285,7 +287,7 @@ describe("client run protocol v1", () => {
|
|
|
285
287
|
stopRequestedAt: "2026-08-28T00:00:05.000Z",
|
|
286
288
|
outcome: {
|
|
287
289
|
type: "cancelled",
|
|
288
|
-
message: "
|
|
290
|
+
message: "You stopped this.",
|
|
289
291
|
},
|
|
290
292
|
});
|
|
291
293
|
expect(projectClientRunLookupV1(storedRun([], "cancelled"))).toMatchObject({
|
|
@@ -297,7 +299,7 @@ describe("client run protocol v1", () => {
|
|
|
297
299
|
).run,
|
|
298
300
|
).toMatchObject({
|
|
299
301
|
status: "cancelled",
|
|
300
|
-
failure: "
|
|
302
|
+
failure: "You stopped this.",
|
|
301
303
|
});
|
|
302
304
|
|
|
303
305
|
expect(() =>
|
|
@@ -745,7 +747,7 @@ describe("client run protocol v1", () => {
|
|
|
745
747
|
],
|
|
746
748
|
recovery: {
|
|
747
749
|
action: "resume",
|
|
748
|
-
message:
|
|
750
|
+
message: RESUMABLE_RUN_MESSAGE_V1,
|
|
749
751
|
},
|
|
750
752
|
},
|
|
751
753
|
],
|
|
@@ -758,10 +760,10 @@ describe("client run protocol v1", () => {
|
|
|
758
760
|
input: "continue",
|
|
759
761
|
status: "reconciliation-required",
|
|
760
762
|
events: projected.runs[0]?.events,
|
|
761
|
-
failure:
|
|
763
|
+
failure: RESUMABLE_RUN_MESSAGE_V1,
|
|
762
764
|
recovery: {
|
|
763
765
|
action: "resume",
|
|
764
|
-
message:
|
|
766
|
+
message: RESUMABLE_RUN_MESSAGE_V1,
|
|
765
767
|
},
|
|
766
768
|
},
|
|
767
769
|
]);
|
|
@@ -1278,14 +1280,18 @@ describe("client run protocol v1", () => {
|
|
|
1278
1280
|
events,
|
|
1279
1281
|
failure: plan.failure,
|
|
1280
1282
|
});
|
|
1283
|
+
// The provider's own words — its name, its status code — stay on the
|
|
1284
|
+
// stored record, which is what the debug surface reads. What crosses to a
|
|
1285
|
+
// chat bubble is the sentence for the outcome, and nothing else.
|
|
1281
1286
|
expect(decodeClientRunLookupV1(structuredClone(lookup))).toMatchObject({
|
|
1282
1287
|
state: "terminal",
|
|
1283
1288
|
run: {
|
|
1284
1289
|
status: "failed",
|
|
1285
|
-
failure:
|
|
1286
|
-
"Bot turn ended with outcome model-error: Ollama Cloud responded 401: invalid api key",
|
|
1290
|
+
failure: RUN_FAILURE_COPY_V1["model-error"],
|
|
1287
1291
|
},
|
|
1288
1292
|
});
|
|
1293
|
+
expect(JSON.stringify(lookup)).not.toContain("Ollama Cloud");
|
|
1294
|
+
expect(JSON.stringify(lookup)).not.toContain("model-error");
|
|
1289
1295
|
});
|
|
1290
1296
|
|
|
1291
1297
|
test("carries rename announcements beside the Turns, and refuses a bad one", () => {
|
package/src/run-protocol.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type BotTurnCompletion,
|
|
25
25
|
type StoredRun,
|
|
26
26
|
} from "./backend-contracts.js";
|
|
27
|
+
import { runFailureCopyV1 } from "./run-failure-copy.js";
|
|
27
28
|
|
|
28
29
|
const MAX_RUN_ID_LENGTH = 128;
|
|
29
30
|
const MAX_TIMESTAMP_LENGTH = 64;
|
|
@@ -54,8 +55,15 @@ export type ClientRunStatusV1 =
|
|
|
54
55
|
| "superseded"
|
|
55
56
|
| "reconciliation-required";
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
// Both are sentences for the person, not descriptions of the mechanism: the
|
|
59
|
+
// wire outcome is what a client with no copy of its own renders verbatim, and
|
|
60
|
+
// "Stopped by an authenticated Stop command" told somebody who pressed Stop
|
|
61
|
+
// about the authentication of their own button press.
|
|
62
|
+
const CANCELLED_RUN_MESSAGE = "You stopped this.";
|
|
58
63
|
const SUPERSEDED_RUN_MESSAGE = "Interrupted by your next message.";
|
|
64
|
+
/** What a Turn waiting on a person's "Try again" says while it waits. */
|
|
65
|
+
export const RESUMABLE_RUN_MESSAGE_V1 =
|
|
66
|
+
"This reply stopped partway. Try again to continue it.";
|
|
59
67
|
|
|
60
68
|
/**
|
|
61
69
|
* Why the Bot declined to admit a Turn. A refusal is an ordinary answer — the
|
|
@@ -836,8 +844,11 @@ export function projectClientRunV1(run: StoredRun): ClientRunV1 {
|
|
|
836
844
|
: status === "failed"
|
|
837
845
|
? ({
|
|
838
846
|
type: "failed",
|
|
847
|
+
// The stored `failure` is a diagnostic and stays one: it is what
|
|
848
|
+
// the debug surface reads. What crosses to a chat bubble is the
|
|
849
|
+
// sentence written for the person — see `runFailureCopyV1`.
|
|
839
850
|
message: truncateWireString(
|
|
840
|
-
run.failure
|
|
851
|
+
runFailureCopyV1({ failure: run.failure, events: run.events }),
|
|
841
852
|
MAX_FAILURE_BYTES,
|
|
842
853
|
),
|
|
843
854
|
...interruptedOutcomeTextV1(run),
|
|
@@ -859,11 +870,10 @@ export function projectClientRunV1(run: StoredRun): ClientRunV1 {
|
|
|
859
870
|
status === "reconciliation-required"
|
|
860
871
|
? ({
|
|
861
872
|
action: "resume",
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
),
|
|
873
|
+
// The stored failure is the diagnostic the debug surface reads; a
|
|
874
|
+
// person offered a "Try again" needs the sentence, not the reason
|
|
875
|
+
// the Bot cannot answer it on its own.
|
|
876
|
+
message: RESUMABLE_RUN_MESSAGE_V1,
|
|
867
877
|
} satisfies ClientRunRecoveryV1)
|
|
868
878
|
: undefined;
|
|
869
879
|
return {
|
package/src/shared.ts
CHANGED
|
@@ -22,6 +22,10 @@ import type {
|
|
|
22
22
|
SendToUserPayloadV1,
|
|
23
23
|
SkillRefV1,
|
|
24
24
|
} from "@frockbot/kernel-contracts";
|
|
25
|
+
import type {
|
|
26
|
+
VoiceDictationObserverV1,
|
|
27
|
+
VoiceDictationSessionV1,
|
|
28
|
+
} from "@frockbot/client-core";
|
|
25
29
|
import type { McpServerStatusViewV1 } from "@frockbot/plugin-mcp/records";
|
|
26
30
|
import type { PackageSettingDefinition } from "@frockbot/kernel-composition";
|
|
27
31
|
import type { ClientSkillCatalogEntryV1 } from "./skill-protocol.js";
|
|
@@ -256,6 +260,12 @@ export interface FrockBotWebData {
|
|
|
256
260
|
modelSource: "bot" | "default" | "none";
|
|
257
261
|
settingsAvailable: boolean;
|
|
258
262
|
connectionsAvailable: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* False when this platform cannot dictate — no transport socket, or a
|
|
265
|
+
* browser with no microphone API. The composer's send button then never
|
|
266
|
+
* changes shape and nothing about it moves.
|
|
267
|
+
*/
|
|
268
|
+
voiceAvailable: boolean;
|
|
259
269
|
activeBotId?: string;
|
|
260
270
|
composerContext?: unknown;
|
|
261
271
|
messages: WebChatMessage[];
|
|
@@ -511,6 +521,13 @@ export interface FrockBotWebData {
|
|
|
511
521
|
skills?: readonly SkillRefV1[],
|
|
512
522
|
): Promise<SendPromptResult>;
|
|
513
523
|
resumeRun(runId: string): Promise<void>;
|
|
524
|
+
/**
|
|
525
|
+
* Opens one dictation session (voice plan D2). `undefined` on a platform
|
|
526
|
+
* whose transport cannot, which is what `voiceAvailable` reports up front.
|
|
527
|
+
*/
|
|
528
|
+
openVoiceDictation(
|
|
529
|
+
observer: VoiceDictationObserverV1,
|
|
530
|
+
): VoiceDictationSessionV1 | undefined;
|
|
514
531
|
/** Sends the durable Stop command for the observed active run. */
|
|
515
532
|
stopRun(): Promise<void>;
|
|
516
533
|
/** Detaches the local observer only; admitted work stays durable. */
|