@frockbot/plugin-shell 0.3.5 → 0.3.6
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 +1 -1
- package/package.json +31 -31
- package/src/backend-authoring.test.ts +72 -0
- package/src/backend-authoring.ts +82 -16
- package/src/backend-configuration.test.ts +6 -4
- package/src/backend-package-catalog.test.ts +23 -3
- package/src/backend-package-catalog.ts +7 -1
- package/src/backend-recovery-integration.test.ts +13 -14
- package/src/backend.ts +28 -4
- package/src/client/AppletCanvas.vue +3 -5
- package/src/client/FrockBotApp.vue +163 -106
- package/src/client/PackageIframeHost.vue +15 -6
- package/src/client/index.test.ts +101 -12
- package/src/client/index.ts +113 -45
- package/src/client/model-presentation.test.ts +4 -2
- package/src/client/model-presentation.ts +2 -2
- package/src/client/styles.css +15 -1
- package/src/run-protocol.ts +15 -3
package/frockbot.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-shell",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -47,41 +47,41 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@cordisjs/plugin-webui": "0.8.2",
|
|
50
|
-
"@frockbot/agent-runtime": "0.3.
|
|
51
|
-
"@frockbot/application-foundation": "0.3.
|
|
52
|
-
"@frockbot/catalog-core": "0.3.
|
|
53
|
-
"@frockbot/client-core": "0.3.
|
|
54
|
-
"@frockbot/client-ui": "0.3.
|
|
55
|
-
"@frockbot/computer-core": "0.3.
|
|
56
|
-
"@frockbot/computer-host-protocol": "0.3.
|
|
57
|
-
"@frockbot/configuration-core": "0.3.
|
|
58
|
-
"@frockbot/connection-core": "0.3.
|
|
59
|
-
"@frockbot/kernel-agent-loop": "0.3.
|
|
60
|
-
"@frockbot/kernel-composition": "0.3.
|
|
61
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
62
|
-
"@frockbot/kernel-do": "0.3.
|
|
63
|
-
"@frockbot/machine-protocol": "0.3.
|
|
64
|
-
"@frockbot/plugin-applets": "0.3.
|
|
65
|
-
"@frockbot/plugin-authoring": "0.3.
|
|
66
|
-
"@frockbot/plugin-bot-template": "0.3.
|
|
67
|
-
"@frockbot/plugin-computer": "0.3.
|
|
68
|
-
"@frockbot/plugin-flock": "0.3.
|
|
69
|
-
"@frockbot/plugin-image": "0.3.
|
|
70
|
-
"@frockbot/plugin-machine-messages": "0.3.
|
|
71
|
-
"@frockbot/plugin-mcp": "0.3.
|
|
72
|
-
"@frockbot/plugin-memory": "0.3.
|
|
73
|
-
"@frockbot/plugin-package-catalog": "0.3.
|
|
74
|
-
"@frockbot/plugin-routines": "0.3.
|
|
75
|
-
"@frockbot/plugin-skills": "0.3.
|
|
76
|
-
"@frockbot/plugin-subagents": "0.3.
|
|
77
|
-
"@frockbot/plugin-user-machine": "0.3.
|
|
78
|
-
"@frockbot/protocol": "0.3.
|
|
50
|
+
"@frockbot/agent-runtime": "0.3.6",
|
|
51
|
+
"@frockbot/application-foundation": "0.3.6",
|
|
52
|
+
"@frockbot/catalog-core": "0.3.6",
|
|
53
|
+
"@frockbot/client-core": "0.3.6",
|
|
54
|
+
"@frockbot/client-ui": "0.3.6",
|
|
55
|
+
"@frockbot/computer-core": "0.3.6",
|
|
56
|
+
"@frockbot/computer-host-protocol": "0.3.6",
|
|
57
|
+
"@frockbot/configuration-core": "0.3.6",
|
|
58
|
+
"@frockbot/connection-core": "0.3.6",
|
|
59
|
+
"@frockbot/kernel-agent-loop": "0.3.6",
|
|
60
|
+
"@frockbot/kernel-composition": "0.3.6",
|
|
61
|
+
"@frockbot/kernel-contracts": "0.3.6",
|
|
62
|
+
"@frockbot/kernel-do": "0.3.6",
|
|
63
|
+
"@frockbot/machine-protocol": "0.3.6",
|
|
64
|
+
"@frockbot/plugin-applets": "0.3.6",
|
|
65
|
+
"@frockbot/plugin-authoring": "0.3.6",
|
|
66
|
+
"@frockbot/plugin-bot-template": "0.3.6",
|
|
67
|
+
"@frockbot/plugin-computer": "0.3.6",
|
|
68
|
+
"@frockbot/plugin-flock": "0.3.6",
|
|
69
|
+
"@frockbot/plugin-image": "0.3.6",
|
|
70
|
+
"@frockbot/plugin-machine-messages": "0.3.6",
|
|
71
|
+
"@frockbot/plugin-mcp": "0.3.6",
|
|
72
|
+
"@frockbot/plugin-memory": "0.3.6",
|
|
73
|
+
"@frockbot/plugin-package-catalog": "0.3.6",
|
|
74
|
+
"@frockbot/plugin-routines": "0.3.6",
|
|
75
|
+
"@frockbot/plugin-skills": "0.3.6",
|
|
76
|
+
"@frockbot/plugin-subagents": "0.3.6",
|
|
77
|
+
"@frockbot/plugin-user-machine": "0.3.6",
|
|
78
|
+
"@frockbot/protocol": "0.3.6",
|
|
79
79
|
"cordis": "4.0.0-rc.8",
|
|
80
80
|
"vue": "3.5.41"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@cloudflare/workers-types": "latest",
|
|
84
|
-
"@frockbot/plugin-tools": "0.3.
|
|
84
|
+
"@frockbot/plugin-tools": "0.3.6",
|
|
85
85
|
"@types/bun": "1.3.6",
|
|
86
86
|
"@types/node": "26.2.0",
|
|
87
87
|
"@vitejs/plugin-vue": "6.0.8",
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
authorshipUndoIntentKey,
|
|
19
19
|
authorshipUndoOutcomeKey,
|
|
20
20
|
AUTHORSHIP_FAILURE_PREFIX,
|
|
21
|
+
classifyAuthoringEffectV1,
|
|
22
|
+
type AuthoringEffectOutcomeV1,
|
|
21
23
|
type AuthoredArtifactRecordV1,
|
|
22
24
|
type AuthoredManifestRecordV1,
|
|
23
25
|
type AuthoringFailureRecordV1,
|
|
@@ -234,9 +236,13 @@ describe("a Bot authoring a Package", () => {
|
|
|
234
236
|
quota?: (request: unknown) => unknown;
|
|
235
237
|
runId?: string;
|
|
236
238
|
currentToolNames?: readonly string[];
|
|
239
|
+
bundleTimeoutMs?: number;
|
|
237
240
|
}) {
|
|
238
241
|
let ids = 0;
|
|
239
242
|
return createPackageAuthoringHost({
|
|
243
|
+
...(options.bundleTimeoutMs === undefined
|
|
244
|
+
? {}
|
|
245
|
+
: { bundleTimeoutMs: options.bundleTimeoutMs }),
|
|
240
246
|
storage,
|
|
241
247
|
composition,
|
|
242
248
|
bundler: options.bundler as never,
|
|
@@ -911,4 +917,70 @@ describe("a Bot authoring a Package", () => {
|
|
|
911
917
|
}),
|
|
912
918
|
);
|
|
913
919
|
});
|
|
920
|
+
|
|
921
|
+
test("a bundler outage leaves the effect retryable, not poisoned", async () => {
|
|
922
|
+
// F11: the "could not be reached" refusal returned without writing any
|
|
923
|
+
// outcome, so `classifyAuthoringEffectV1` called that effectId `unknown`
|
|
924
|
+
// forever — "it will not be bundled again". Since the effectId is
|
|
925
|
+
// run + Package + source hash, retrying the identical source in the same
|
|
926
|
+
// run could never succeed.
|
|
927
|
+
const effectId = "author-0123456789abcdef";
|
|
928
|
+
const offline = {
|
|
929
|
+
calls: 0,
|
|
930
|
+
bundle() {
|
|
931
|
+
offline.calls += 1;
|
|
932
|
+
return Promise.reject(
|
|
933
|
+
new Error('Worker "frockbot-cloudflare-bundler" not found'),
|
|
934
|
+
);
|
|
935
|
+
},
|
|
936
|
+
};
|
|
937
|
+
const refusal = await host({ bundler: offline as never }).author(
|
|
938
|
+
requestFor(),
|
|
939
|
+
);
|
|
940
|
+
expect(refusal.status).toBe("refused");
|
|
941
|
+
|
|
942
|
+
const outcome = storage.values.get(
|
|
943
|
+
authorshipArtifactKey(effectId),
|
|
944
|
+
) as AuthoringEffectOutcomeV1;
|
|
945
|
+
expect(outcome).toMatchObject({ status: "unreachable", effectId });
|
|
946
|
+
expect(
|
|
947
|
+
classifyAuthoringEffectV1({
|
|
948
|
+
intent: storage.values.get(authorshipIntentKey(effectId)) as never,
|
|
949
|
+
outcome,
|
|
950
|
+
}),
|
|
951
|
+
).toEqual({ kind: "fresh" });
|
|
952
|
+
|
|
953
|
+
// And the identical source, in the same run, now goes through.
|
|
954
|
+
const recovered = await host({
|
|
955
|
+
bundler: countingBundler((id) => bundledResult(id)),
|
|
956
|
+
}).author(requestFor());
|
|
957
|
+
expect(recovered.status).toBe("authored");
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
test("a bundler that never answers is refused at the deadline", async () => {
|
|
961
|
+
// F10: `bundle()` was awaited with no deadline and no AbortSignal, and the
|
|
962
|
+
// bundler runs esbuild-wasm inline — a hang there hung this tool call and
|
|
963
|
+
// the Turn around it, indefinitely.
|
|
964
|
+
const hanging = {
|
|
965
|
+
calls: 0,
|
|
966
|
+
bundle() {
|
|
967
|
+
hanging.calls += 1;
|
|
968
|
+
return new Promise<never>(() => {});
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
const outcome = await host({
|
|
972
|
+
bundler: hanging as never,
|
|
973
|
+
bundleTimeoutMs: 10,
|
|
974
|
+
}).author(requestFor());
|
|
975
|
+
|
|
976
|
+
expect(outcome).toMatchObject({ status: "refused" });
|
|
977
|
+
if (outcome.status !== "refused") return;
|
|
978
|
+
expect(outcome.reason).toContain(
|
|
979
|
+
"the Package bundler could not be reached: the Package bundler did not answer within 10ms",
|
|
980
|
+
);
|
|
981
|
+
// Nothing ran, so the effect stays retryable rather than poisoned (F11).
|
|
982
|
+
expect(
|
|
983
|
+
storage.values.get(authorshipArtifactKey("author-0123456789abcdef")),
|
|
984
|
+
).toMatchObject({ status: "unreachable" });
|
|
985
|
+
});
|
|
914
986
|
});
|
package/src/backend-authoring.ts
CHANGED
|
@@ -143,12 +143,45 @@ export interface PackageAuthoringHostOptions {
|
|
|
143
143
|
};
|
|
144
144
|
now?(): Date;
|
|
145
145
|
newId?(): string;
|
|
146
|
+
/** Overrides `PACKAGE_BUNDLE_TIMEOUT_MS_V1`; tests drive the deadline. */
|
|
147
|
+
bundleTimeoutMs?: number;
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
function errorMessage(error: unknown): string {
|
|
149
151
|
return error instanceof Error ? error.message : String(error);
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
/**
|
|
155
|
+
* How long the Package bundler has to answer. It runs esbuild-wasm inline, so
|
|
156
|
+
* a hang there is a hang of this tool call and of the Turn around it; the
|
|
157
|
+
* service binding itself imposes no deadline (finding F10). Generous, because
|
|
158
|
+
* a cold `createWorker` is slow, and far short of any Turn-level bound.
|
|
159
|
+
*/
|
|
160
|
+
const PACKAGE_BUNDLE_TIMEOUT_MS_V1 = 60_000;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* `work`, or a rejection at the deadline. The pending work is abandoned, not
|
|
164
|
+
* cancelled: the bundler is stateless and side-effect free (its caller owns
|
|
165
|
+
* every durable write), so a late answer changes nothing.
|
|
166
|
+
*/
|
|
167
|
+
async function withDeadline<T>(
|
|
168
|
+
work: Promise<T>,
|
|
169
|
+
milliseconds: number,
|
|
170
|
+
message: string,
|
|
171
|
+
): Promise<T> {
|
|
172
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
173
|
+
try {
|
|
174
|
+
return await Promise.race([
|
|
175
|
+
work,
|
|
176
|
+
new Promise<never>((_resolve, reject) => {
|
|
177
|
+
timer = setTimeout(() => reject(new Error(message)), milliseconds);
|
|
178
|
+
}),
|
|
179
|
+
]);
|
|
180
|
+
} finally {
|
|
181
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
152
185
|
/**
|
|
153
186
|
* Builds the authoring seam one admitted Turn runs under. Constructed per
|
|
154
187
|
* Turn because provenance names the run and turn that produced the artifact.
|
|
@@ -691,18 +724,40 @@ export function createPackageAuthoringHost(
|
|
|
691
724
|
let bundled;
|
|
692
725
|
try {
|
|
693
726
|
// Never inside a storage transaction: bundler CPU is the bundler's.
|
|
727
|
+
// Bounded, because the bundler runs esbuild-wasm inline: one that
|
|
728
|
+
// hangs rather than rejects would hang this tool call and the Turn
|
|
729
|
+
// with it (F10). An expiry takes the same path as unreachability —
|
|
730
|
+
// nothing ran either way.
|
|
731
|
+
const deadline =
|
|
732
|
+
options.bundleTimeoutMs ?? PACKAGE_BUNDLE_TIMEOUT_MS_V1;
|
|
694
733
|
bundled = decodePackageBundleResultV1(
|
|
695
|
-
await
|
|
734
|
+
await withDeadline(
|
|
735
|
+
options.bundler.bundle(bundleRequest),
|
|
736
|
+
deadline,
|
|
737
|
+
`the Package bundler did not answer within ${deadline}ms`,
|
|
738
|
+
),
|
|
696
739
|
);
|
|
697
740
|
} catch (error) {
|
|
698
|
-
|
|
699
|
-
|
|
741
|
+
const failure = await recordFailure({
|
|
742
|
+
effectId,
|
|
743
|
+
packageId,
|
|
744
|
+
phase: "bundle",
|
|
745
|
+
reason: `the Package bundler could not be reached: ${errorMessage(error)}`,
|
|
746
|
+
});
|
|
747
|
+
// The bundler never answered, so nothing about this effect is
|
|
748
|
+
// uncertain. Recording that explicitly is what lets an identical
|
|
749
|
+
// retry in the same run proceed instead of classifying `unknown`
|
|
750
|
+
// forever (F11).
|
|
751
|
+
await options.storage.put({
|
|
752
|
+
[authorshipArtifactKey(effectId)]: {
|
|
753
|
+
schemaVersion: 1,
|
|
754
|
+
status: "unreachable",
|
|
700
755
|
effectId,
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
);
|
|
756
|
+
failureId: failure.failureId,
|
|
757
|
+
reason: failure.reason,
|
|
758
|
+
} satisfies AuthoringEffectOutcomeV1,
|
|
759
|
+
});
|
|
760
|
+
return refused(failure);
|
|
706
761
|
}
|
|
707
762
|
if (bundled.status === "failed") {
|
|
708
763
|
const failure = await recordFailure({
|
|
@@ -744,15 +799,26 @@ export function createPackageAuthoringHost(
|
|
|
744
799
|
);
|
|
745
800
|
}));
|
|
746
801
|
if (uiMismatch || (uiPages === undefined && returnedPages.length > 0)) {
|
|
747
|
-
|
|
748
|
-
|
|
802
|
+
const failure = await recordFailure({
|
|
803
|
+
effectId,
|
|
804
|
+
packageId,
|
|
805
|
+
phase: "bundle",
|
|
806
|
+
reason:
|
|
807
|
+
"the Package bundler returned UI bytes that do not match the recorded manifest",
|
|
808
|
+
});
|
|
809
|
+
// The bundler did answer, and deterministically: the same source
|
|
810
|
+
// would mismatch again. That is a settled failure, not an unknown —
|
|
811
|
+
// it too used to leave the effect with no outcome at all (F11).
|
|
812
|
+
await options.storage.put({
|
|
813
|
+
[authorshipArtifactKey(effectId)]: {
|
|
814
|
+
schemaVersion: 1,
|
|
815
|
+
status: "failed",
|
|
749
816
|
effectId,
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
);
|
|
817
|
+
failureId: failure.failureId,
|
|
818
|
+
reason: failure.reason,
|
|
819
|
+
} satisfies AuthoringEffectOutcomeV1,
|
|
820
|
+
});
|
|
821
|
+
return refused(failure);
|
|
756
822
|
}
|
|
757
823
|
// Content-addressed and immutable: writing the same hash twice is a
|
|
758
824
|
// no-op, so the object write is safe to repeat and the record is not.
|
|
@@ -702,10 +702,12 @@ describe("generic per-Turn model resolution", () => {
|
|
|
702
702
|
acceptedAt: "2026-09-02T00:01:00.000Z",
|
|
703
703
|
text: "must fail",
|
|
704
704
|
}),
|
|
705
|
-
).rejects.toThrow("
|
|
705
|
+
).rejects.toThrow("Turn this model's plugin back on in Plugins");
|
|
706
706
|
expect(await bot.storage.get(`run:${runId}`)).toMatchObject({
|
|
707
707
|
status: "failed",
|
|
708
|
-
failure: expect.stringContaining(
|
|
708
|
+
failure: expect.stringContaining(
|
|
709
|
+
"Turn this model's plugin back on in Plugins",
|
|
710
|
+
),
|
|
709
711
|
});
|
|
710
712
|
}
|
|
711
713
|
|
|
@@ -722,10 +724,10 @@ describe("generic per-Turn model resolution", () => {
|
|
|
722
724
|
acceptedAt: "2026-09-02T00:02:00.000Z",
|
|
723
725
|
text: "must fail",
|
|
724
726
|
}),
|
|
725
|
-
).rejects.toThrow("
|
|
727
|
+
).rejects.toThrow("needs reconnecting");
|
|
726
728
|
expect(await bot.storage.get(`run:${runId}`)).toMatchObject({
|
|
727
729
|
status: "failed",
|
|
728
|
-
failure: expect.stringContaining("
|
|
730
|
+
failure: expect.stringContaining("needs reconnecting"),
|
|
729
731
|
});
|
|
730
732
|
}
|
|
731
733
|
});
|
|
@@ -147,7 +147,12 @@ async function bootstrap(): Promise<CompositionGenerationV1> {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
async function fixture(
|
|
150
|
-
options: {
|
|
150
|
+
options: {
|
|
151
|
+
connected?: boolean;
|
|
152
|
+
uiAvailable?: boolean;
|
|
153
|
+
/** A deployment with no published Catalog pins no generation. */
|
|
154
|
+
pinned?: boolean;
|
|
155
|
+
} = {},
|
|
151
156
|
) {
|
|
152
157
|
const log: string[] = [];
|
|
153
158
|
const records = new Map<string, unknown>();
|
|
@@ -220,8 +225,9 @@ async function fixture(
|
|
|
220
225
|
},
|
|
221
226
|
]
|
|
222
227
|
: [],
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
...(options.pinned === false
|
|
229
|
+
? {}
|
|
230
|
+
: { catalogGeneration: "catalog-1", catalogIndexHash: "d".repeat(64) }),
|
|
225
231
|
};
|
|
226
232
|
const commands: UserConfigurationCommandV1[] = [];
|
|
227
233
|
const userAuthority = {
|
|
@@ -306,6 +312,20 @@ describe("Bot Package Catalog host", () => {
|
|
|
306
312
|
});
|
|
307
313
|
});
|
|
308
314
|
|
|
315
|
+
test("explains an unpinned Catalog in words a person can act on", async () => {
|
|
316
|
+
const { host } = await fixture({ pinned: false });
|
|
317
|
+
// F4: an unpinned User used to be told "this User has no pinned Package
|
|
318
|
+
// Catalog generation", which the Bot then relayed verbatim.
|
|
319
|
+
for (const call of [
|
|
320
|
+
() => host.search({ query: "shipping" }),
|
|
321
|
+
() => host.inspect({ catalogId: "parcel-tracking" }),
|
|
322
|
+
]) {
|
|
323
|
+
await expect(call()).rejects.toThrow(
|
|
324
|
+
"No Package Catalog is published for this deployment, so there is nothing to search or install",
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
309
329
|
test("persists the iframe manifest before the User effect, appends a summary generation, and replays", async () => {
|
|
310
330
|
const test = await fixture();
|
|
311
331
|
const request = {
|
|
@@ -145,7 +145,13 @@ function requiredCatalogPin(user: UserSettingsViewV1): {
|
|
|
145
145
|
indexHash: string;
|
|
146
146
|
} {
|
|
147
147
|
if (!user.catalogGeneration || !user.catalogIndexHash) {
|
|
148
|
-
|
|
148
|
+
// A User is pinned on the first configuration read that finds a published
|
|
149
|
+
// Catalog, so an absent pin means the deployment has none to find. Say
|
|
150
|
+
// that, rather than an internal sentence about pinning the Bot then
|
|
151
|
+
// relays to the person who asked.
|
|
152
|
+
throw new Error(
|
|
153
|
+
"No Package Catalog is published for this deployment, so there is nothing to search or install",
|
|
154
|
+
);
|
|
149
155
|
}
|
|
150
156
|
return {
|
|
151
157
|
generation: user.catalogGeneration,
|
|
@@ -626,7 +626,11 @@ describe("Bot recovery", () => {
|
|
|
626
626
|
expect(await recoveredAgain.listNotifications()).toEqual(notifications);
|
|
627
627
|
});
|
|
628
628
|
|
|
629
|
-
|
|
629
|
+
// ADR 0028. This used to park the run and hand the person a Resolve button.
|
|
630
|
+
// `provider-1` has no retrieval — none of the providers this deployment
|
|
631
|
+
// ships does — so there was never an answer to resolve it with, and the Bot
|
|
632
|
+
// stayed wedged behind it.
|
|
633
|
+
test("settles an unresolved request no provider can be asked about", async () => {
|
|
630
634
|
const storage = new MemoryStorage();
|
|
631
635
|
const settings = initializeBotSettingsV1("primary");
|
|
632
636
|
const events = [
|
|
@@ -690,19 +694,14 @@ describe("Bot recovery", () => {
|
|
|
690
694
|
env: {} as never,
|
|
691
695
|
});
|
|
692
696
|
|
|
693
|
-
await
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}),
|
|
702
|
-
],
|
|
703
|
-
page: { truncated: false },
|
|
704
|
-
});
|
|
705
|
-
expect(storage.values.get("active-run")).toBe("run-lost-marker");
|
|
697
|
+
await recovered.listRuns();
|
|
698
|
+
|
|
699
|
+
const settled = storage.values.get("run:run-lost-marker") as StoredRun;
|
|
700
|
+
expect(settled.status).toBe("failed");
|
|
701
|
+
expect(settled.failure).toContain("stopped partway");
|
|
702
|
+
// The Bot is released: nothing is holding the next Turn behind a decision
|
|
703
|
+
// nobody can make.
|
|
704
|
+
expect(storage.values.get("active-run")).toBeUndefined();
|
|
706
705
|
expect(storage.alarmAt).toBeUndefined();
|
|
707
706
|
});
|
|
708
707
|
|
package/src/backend.ts
CHANGED
|
@@ -446,6 +446,25 @@ import {
|
|
|
446
446
|
} from "./unread.js";
|
|
447
447
|
import { defineBotBackendContribution } from "@frockbot/kernel-contracts/contributions";
|
|
448
448
|
|
|
449
|
+
/**
|
|
450
|
+
* The providers that can be asked what happened to a model request they never
|
|
451
|
+
* answered — that is, the ones whose Package registers an
|
|
452
|
+
* `LlmReconciliationCapability` (ADR 0028).
|
|
453
|
+
*
|
|
454
|
+
* It is a list rather than a lookup because recovery consults it inside the
|
|
455
|
+
* durable transaction that settles the run, where nothing may be mounted or
|
|
456
|
+
* awaited. The cost is that a provider Package which gains retrieval has to
|
|
457
|
+
* name itself here; the ADR carries that obligation, and the failure mode of
|
|
458
|
+
* forgetting is a Turn settled as failed rather than one wedged forever, which
|
|
459
|
+
* is the direction this deployment wants to be wrong in.
|
|
460
|
+
*
|
|
461
|
+
* Today: the in-process foundation provider, and nothing else. Ollama Cloud
|
|
462
|
+
* exposes no provider-bound retrieval (ADR 0010) and neither does Flock AI.
|
|
463
|
+
*/
|
|
464
|
+
const RECONCILING_PROVIDER_IDS_V1: ReadonlySet<string> = new Set([
|
|
465
|
+
"foundation",
|
|
466
|
+
]);
|
|
467
|
+
|
|
449
468
|
export const BOT_CONFIGURATION_KEY = "bot-configuration";
|
|
450
469
|
const CONFIGURATION_RECEIPT_PREFIX = "configuration-receipt:";
|
|
451
470
|
const STOP_RECEIPT_PREFIX = "stop-receipt:";
|
|
@@ -784,6 +803,8 @@ export class ShellBotBackendContribution {
|
|
|
784
803
|
deferScheduledWork: (transaction) =>
|
|
785
804
|
this.deferScheduledWork(transaction),
|
|
786
805
|
settleScheduledWork: () => this.settleScheduledWork(),
|
|
806
|
+
providerReconciles: (providerId) =>
|
|
807
|
+
RECONCILING_PROVIDER_IDS_V1.has(providerId),
|
|
787
808
|
},
|
|
788
809
|
});
|
|
789
810
|
}
|
|
@@ -4572,13 +4593,13 @@ export class ShellBotBackendContribution {
|
|
|
4572
4593
|
if (!effectiveModel) {
|
|
4573
4594
|
throw new Error(
|
|
4574
4595
|
effective.binding?.failure ??
|
|
4575
|
-
"No model is
|
|
4596
|
+
"No model is set up yet. Choose one in Models.",
|
|
4576
4597
|
);
|
|
4577
4598
|
}
|
|
4578
4599
|
const binding: ResolvedModelBindingV1 = effective.binding ?? {
|
|
4579
4600
|
model: structuredClone(effectiveModel),
|
|
4580
4601
|
state: "unavailable",
|
|
4581
|
-
failure: "
|
|
4602
|
+
failure: "This Bot's model isn't available. Pick one in Models.",
|
|
4582
4603
|
};
|
|
4583
4604
|
if (
|
|
4584
4605
|
binding.state === "unavailable" ||
|
|
@@ -4587,7 +4608,8 @@ export class ShellBotBackendContribution {
|
|
|
4587
4608
|
!binding.packageId
|
|
4588
4609
|
) {
|
|
4589
4610
|
throw new Error(
|
|
4590
|
-
binding.failure ??
|
|
4611
|
+
binding.failure ??
|
|
4612
|
+
"This Bot's model isn't available. Pick one in Models.",
|
|
4591
4613
|
);
|
|
4592
4614
|
}
|
|
4593
4615
|
if (
|
|
@@ -4600,7 +4622,9 @@ export class ShellBotBackendContribution {
|
|
|
4600
4622
|
admittedRequest.modelBinding.connectionGeneration !==
|
|
4601
4623
|
binding.connection.generation)
|
|
4602
4624
|
) {
|
|
4603
|
-
throw new Error(
|
|
4625
|
+
throw new Error(
|
|
4626
|
+
"This Bot's model changed mid-reply. Send your message again.",
|
|
4627
|
+
);
|
|
4604
4628
|
}
|
|
4605
4629
|
const bindingPackageId = binding.packageId;
|
|
4606
4630
|
agentPackages.push(
|
|
@@ -147,7 +147,7 @@ function generationLabel(generationId: string): string {
|
|
|
147
147
|
hour: "numeric",
|
|
148
148
|
minute: "2-digit",
|
|
149
149
|
});
|
|
150
|
-
return `Live since ${when}
|
|
150
|
+
return `Live since ${when}`;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
function selectTab(next: "app" | "code"): void {
|
|
@@ -199,10 +199,8 @@ async function clearFocus(): Promise<void> {
|
|
|
199
199
|
/></span>
|
|
200
200
|
<div class="applet-canvas-title">
|
|
201
201
|
<strong>{{ applet?.displayName ?? "Applet" }}</strong>
|
|
202
|
-
<small v-if="viewer"
|
|
203
|
-
|
|
204
|
-
}}</small>
|
|
205
|
-
<small v-else>No published version yet</small>
|
|
202
|
+
<small v-if="viewer">{{ generationLabel(viewer.generationId) }}</small>
|
|
203
|
+
<small v-else>Not published yet</small>
|
|
206
204
|
</div>
|
|
207
205
|
<div
|
|
208
206
|
v-if="canShowApp"
|