@frockbot/plugin-shell 0.3.23 → 0.3.24
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 +32 -32
- package/src/backend-composition.test.ts +221 -0
- package/src/backend-composition.ts +90 -0
- package/src/backend.ts +40 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-shell",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -52,42 +52,42 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@cordisjs/plugin-webui": "0.8.2",
|
|
55
|
-
"@frockbot/agent-runtime": "0.3.
|
|
56
|
-
"@frockbot/application-foundation": "0.3.
|
|
57
|
-
"@frockbot/catalog-core": "0.3.
|
|
58
|
-
"@frockbot/client-core": "0.3.
|
|
59
|
-
"@frockbot/client-ui": "0.3.
|
|
60
|
-
"@frockbot/computer-core": "0.3.
|
|
61
|
-
"@frockbot/computer-host-protocol": "0.3.
|
|
62
|
-
"@frockbot/configuration-core": "0.3.
|
|
63
|
-
"@frockbot/connection-core": "0.3.
|
|
64
|
-
"@frockbot/kernel-agent-loop": "0.3.
|
|
65
|
-
"@frockbot/kernel-composition": "0.3.
|
|
66
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
67
|
-
"@frockbot/kernel-do": "0.3.
|
|
68
|
-
"@frockbot/machine-protocol": "0.3.
|
|
69
|
-
"@frockbot/plugin-applets": "0.3.
|
|
70
|
-
"@frockbot/plugin-authoring": "0.3.
|
|
71
|
-
"@frockbot/plugin-bot-template": "0.3.
|
|
72
|
-
"@frockbot/plugin-computer": "0.3.
|
|
73
|
-
"@frockbot/plugin-flock": "0.3.
|
|
74
|
-
"@frockbot/plugin-image": "0.3.
|
|
75
|
-
"@frockbot/plugin-machine-messages": "0.3.
|
|
76
|
-
"@frockbot/plugin-mcp": "0.3.
|
|
77
|
-
"@frockbot/plugin-memory": "0.3.
|
|
78
|
-
"@frockbot/plugin-package-catalog": "0.3.
|
|
79
|
-
"@frockbot/plugin-routines": "0.3.
|
|
80
|
-
"@frockbot/plugin-skills": "0.3.
|
|
81
|
-
"@frockbot/plugin-subagents": "0.3.
|
|
82
|
-
"@frockbot/plugin-user-machine": "0.3.
|
|
83
|
-
"@frockbot/protocol": "0.3.
|
|
55
|
+
"@frockbot/agent-runtime": "0.3.24",
|
|
56
|
+
"@frockbot/application-foundation": "0.3.24",
|
|
57
|
+
"@frockbot/catalog-core": "0.3.24",
|
|
58
|
+
"@frockbot/client-core": "0.3.24",
|
|
59
|
+
"@frockbot/client-ui": "0.3.24",
|
|
60
|
+
"@frockbot/computer-core": "0.3.24",
|
|
61
|
+
"@frockbot/computer-host-protocol": "0.3.24",
|
|
62
|
+
"@frockbot/configuration-core": "0.3.24",
|
|
63
|
+
"@frockbot/connection-core": "0.3.24",
|
|
64
|
+
"@frockbot/kernel-agent-loop": "0.3.24",
|
|
65
|
+
"@frockbot/kernel-composition": "0.3.24",
|
|
66
|
+
"@frockbot/kernel-contracts": "0.3.24",
|
|
67
|
+
"@frockbot/kernel-do": "0.3.24",
|
|
68
|
+
"@frockbot/machine-protocol": "0.3.24",
|
|
69
|
+
"@frockbot/plugin-applets": "0.3.24",
|
|
70
|
+
"@frockbot/plugin-authoring": "0.3.24",
|
|
71
|
+
"@frockbot/plugin-bot-template": "0.3.24",
|
|
72
|
+
"@frockbot/plugin-computer": "0.3.24",
|
|
73
|
+
"@frockbot/plugin-flock": "0.3.24",
|
|
74
|
+
"@frockbot/plugin-image": "0.3.24",
|
|
75
|
+
"@frockbot/plugin-machine-messages": "0.3.24",
|
|
76
|
+
"@frockbot/plugin-mcp": "0.3.24",
|
|
77
|
+
"@frockbot/plugin-memory": "0.3.24",
|
|
78
|
+
"@frockbot/plugin-package-catalog": "0.3.24",
|
|
79
|
+
"@frockbot/plugin-routines": "0.3.24",
|
|
80
|
+
"@frockbot/plugin-skills": "0.3.24",
|
|
81
|
+
"@frockbot/plugin-subagents": "0.3.24",
|
|
82
|
+
"@frockbot/plugin-user-machine": "0.3.24",
|
|
83
|
+
"@frockbot/protocol": "0.3.24",
|
|
84
84
|
"cordis": "4.0.0-rc.8",
|
|
85
85
|
"vue": "3.5.41"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@cloudflare/workers-types": "latest",
|
|
89
|
-
"@frockbot/plugin-prompt": "0.3.
|
|
90
|
-
"@frockbot/plugin-tools": "0.3.
|
|
89
|
+
"@frockbot/plugin-prompt": "0.3.24",
|
|
90
|
+
"@frockbot/plugin-tools": "0.3.24",
|
|
91
91
|
"@types/bun": "1.3.6",
|
|
92
92
|
"@types/node": "26.2.0",
|
|
93
93
|
"@vitejs/plugin-vue": "6.0.8",
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ApplicationPlan } from "@frockbot/kernel-composition/compiler";
|
|
3
|
+
import {
|
|
4
|
+
compositionArtifactSetHashV1,
|
|
5
|
+
decodeCompositionGenerationV1,
|
|
6
|
+
type CompositionGenerationV1,
|
|
7
|
+
type CompositionMemberV1,
|
|
8
|
+
} from "@frockbot/kernel-composition/generation";
|
|
9
|
+
import {
|
|
10
|
+
bootstrapCompositionGeneration,
|
|
11
|
+
resolveDeploymentCompositionV1,
|
|
12
|
+
DEPLOYMENT_FOLLOW_SUMMARY_V1,
|
|
13
|
+
} from "./backend-composition.js";
|
|
14
|
+
|
|
15
|
+
const ARTIFACT_A = {
|
|
16
|
+
contentHash: "a".repeat(64),
|
|
17
|
+
size: 10,
|
|
18
|
+
mediaType: "application/javascript" as const,
|
|
19
|
+
bundlerVersion: "1",
|
|
20
|
+
};
|
|
21
|
+
const ARTIFACT_B = { ...ARTIFACT_A, contentHash: "b".repeat(64) };
|
|
22
|
+
|
|
23
|
+
/** A compiled application: the shell in-process, the Applets as an artifact. */
|
|
24
|
+
function plan(input: {
|
|
25
|
+
appletsManifest: unknown;
|
|
26
|
+
appletsArtifact?: typeof ARTIFACT_A;
|
|
27
|
+
extra?: boolean;
|
|
28
|
+
}): ApplicationPlan {
|
|
29
|
+
return {
|
|
30
|
+
packages: [
|
|
31
|
+
{
|
|
32
|
+
id: "shell",
|
|
33
|
+
specifier: "@frockbot/plugin-shell",
|
|
34
|
+
version: "1.0.0",
|
|
35
|
+
manifest: { id: "shell" },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "applets",
|
|
39
|
+
specifier: "@frockbot/plugin-applets",
|
|
40
|
+
version: "1.0.0",
|
|
41
|
+
manifest: input.appletsManifest,
|
|
42
|
+
artifact: input.appletsArtifact ?? ARTIFACT_A,
|
|
43
|
+
},
|
|
44
|
+
...(input.extra
|
|
45
|
+
? [
|
|
46
|
+
{
|
|
47
|
+
id: "extra",
|
|
48
|
+
specifier: "@frockbot/plugin-extra",
|
|
49
|
+
version: "1.0.0",
|
|
50
|
+
manifest: { id: "extra" },
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
: []),
|
|
54
|
+
],
|
|
55
|
+
} as unknown as ApplicationPlan;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function store(current: CompositionGenerationV1) {
|
|
59
|
+
const proposed: CompositionGenerationV1[] = [];
|
|
60
|
+
return {
|
|
61
|
+
proposed,
|
|
62
|
+
current: () => Promise.resolve(current),
|
|
63
|
+
propose: (generation: CompositionGenerationV1) => {
|
|
64
|
+
proposed.push(generation);
|
|
65
|
+
return Promise.resolve();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The generation a Bot pinned under yesterday's deployment, plus a User member. */
|
|
71
|
+
async function pinned(
|
|
72
|
+
deployment: ApplicationPlan,
|
|
73
|
+
extraMembers: CompositionMemberV1[] = [],
|
|
74
|
+
): Promise<CompositionGenerationV1> {
|
|
75
|
+
const bootstrap = await bootstrapCompositionGeneration(
|
|
76
|
+
deployment,
|
|
77
|
+
"2026-09-04T09:00:00.000Z",
|
|
78
|
+
);
|
|
79
|
+
const members = [...bootstrap.members, ...extraMembers].sort((a, b) =>
|
|
80
|
+
a.packageId.localeCompare(b.packageId),
|
|
81
|
+
);
|
|
82
|
+
const applets = [
|
|
83
|
+
{
|
|
84
|
+
kind: "applet" as const,
|
|
85
|
+
appletId: "user-1.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
86
|
+
generationId: "g1",
|
|
87
|
+
tools: [
|
|
88
|
+
{
|
|
89
|
+
name: "add_todo",
|
|
90
|
+
description: "Add",
|
|
91
|
+
inputSchema: { type: "object" },
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
provenance: {
|
|
95
|
+
kind: "user" as const,
|
|
96
|
+
packageId: "user-1.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
97
|
+
version: "g1",
|
|
98
|
+
userId: "user-1",
|
|
99
|
+
authoredAt: "2026-09-04T09:00:00.000Z",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
const artifactSetHash = await compositionArtifactSetHashV1(members, applets);
|
|
104
|
+
return decodeCompositionGenerationV1({
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
generationId: `2026-09-04T09:00:00.000Z:${artifactSetHash.slice(0, 16)}`,
|
|
107
|
+
artifactSetHash,
|
|
108
|
+
createdAt: "2026-09-04T09:00:00.000Z",
|
|
109
|
+
origin: { kind: "bootstrap" },
|
|
110
|
+
members,
|
|
111
|
+
applets,
|
|
112
|
+
status: "active",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const USER_MEMBER: CompositionMemberV1 = {
|
|
117
|
+
packageId: "user-1.notes",
|
|
118
|
+
specifier: "frockbot:user-1.notes",
|
|
119
|
+
version: "3",
|
|
120
|
+
manifestHash: "d".repeat(64),
|
|
121
|
+
provenance: {
|
|
122
|
+
kind: "user",
|
|
123
|
+
packageId: "user-1.notes",
|
|
124
|
+
version: "3",
|
|
125
|
+
userId: "user-1",
|
|
126
|
+
authoredAt: "2026-09-04T10:00:00.000Z",
|
|
127
|
+
},
|
|
128
|
+
artifact: { ...ARTIFACT_A, contentHash: "e".repeat(64) },
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
describe("a Bot's built-in members follow the deployment", () => {
|
|
132
|
+
test("an unchanged deployment proposes nothing", async () => {
|
|
133
|
+
const yesterday = plan({ appletsManifest: { id: "applets", v: 1 } });
|
|
134
|
+
const composition = store(await pinned(yesterday, [USER_MEMBER]));
|
|
135
|
+
const result = await resolveDeploymentCompositionV1({
|
|
136
|
+
plan: yesterday,
|
|
137
|
+
composition,
|
|
138
|
+
now: new Date("2026-09-05T07:30:00.000Z"),
|
|
139
|
+
});
|
|
140
|
+
expect(result).toBeUndefined();
|
|
141
|
+
expect(composition.proposed).toHaveLength(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("a changed manifest or artifact is a new pinned generation with the old one as parent", async () => {
|
|
145
|
+
const yesterday = plan({ appletsManifest: { id: "applets", v: 1 } });
|
|
146
|
+
const current = await pinned(yesterday, [USER_MEMBER]);
|
|
147
|
+
const composition = store(current);
|
|
148
|
+
const today = plan({
|
|
149
|
+
appletsManifest: { id: "applets", v: 2 },
|
|
150
|
+
appletsArtifact: ARTIFACT_B,
|
|
151
|
+
});
|
|
152
|
+
const result = await resolveDeploymentCompositionV1({
|
|
153
|
+
plan: today,
|
|
154
|
+
composition,
|
|
155
|
+
now: new Date("2026-09-05T07:30:00.000Z"),
|
|
156
|
+
});
|
|
157
|
+
expect(result).toBeDefined();
|
|
158
|
+
expect(composition.proposed).toEqual([result!]);
|
|
159
|
+
expect(result!.status).toBe("pending");
|
|
160
|
+
expect(result!.parentGenerationId).toBe(current.generationId);
|
|
161
|
+
expect(result!.summary).toBe(DEPLOYMENT_FOLLOW_SUMMARY_V1);
|
|
162
|
+
const deployed = await bootstrapCompositionGeneration(
|
|
163
|
+
today,
|
|
164
|
+
result!.createdAt,
|
|
165
|
+
);
|
|
166
|
+
const applets = result!.members.find((m) => m.packageId === "applets");
|
|
167
|
+
expect(applets).toEqual(
|
|
168
|
+
deployed.members.find((m) => m.packageId === "applets"),
|
|
169
|
+
);
|
|
170
|
+
// The shell did not change, the User's own member is carried over
|
|
171
|
+
// untouched, and the Applet members stay pinned.
|
|
172
|
+
expect(result!.members.find((m) => m.packageId === "shell")).toEqual(
|
|
173
|
+
current.members.find((m) => m.packageId === "shell"),
|
|
174
|
+
);
|
|
175
|
+
expect(result!.members.find((m) => m.packageId === "user-1.notes")).toEqual(
|
|
176
|
+
USER_MEMBER,
|
|
177
|
+
);
|
|
178
|
+
expect(result!.applets).toEqual(current.applets);
|
|
179
|
+
expect(result!.artifactSetHash).toBe(
|
|
180
|
+
await compositionArtifactSetHashV1(result!.members, result!.applets),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("a built-in Package the deployment added or dropped changes the set", async () => {
|
|
185
|
+
const yesterday = plan({ appletsManifest: { id: "applets", v: 1 } });
|
|
186
|
+
const composition = store(await pinned(yesterday));
|
|
187
|
+
const today = plan({
|
|
188
|
+
appletsManifest: { id: "applets", v: 1 },
|
|
189
|
+
extra: true,
|
|
190
|
+
});
|
|
191
|
+
const added = await resolveDeploymentCompositionV1({
|
|
192
|
+
plan: today,
|
|
193
|
+
composition,
|
|
194
|
+
});
|
|
195
|
+
expect(added!.members.map((m) => m.packageId)).toEqual([
|
|
196
|
+
"applets",
|
|
197
|
+
"extra",
|
|
198
|
+
"shell",
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
const dropped = await resolveDeploymentCompositionV1({
|
|
202
|
+
plan: yesterday,
|
|
203
|
+
composition: store(await pinned(today)),
|
|
204
|
+
});
|
|
205
|
+
expect(dropped!.members.map((m) => m.packageId)).toEqual([
|
|
206
|
+
"applets",
|
|
207
|
+
"shell",
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("a version bump alone does not spend a generation", async () => {
|
|
212
|
+
const yesterday = plan({ appletsManifest: { id: "applets", v: 1 } });
|
|
213
|
+
const composition = store(await pinned(yesterday));
|
|
214
|
+
const bumped = {
|
|
215
|
+
packages: yesterday.packages.map((pkg) => ({ ...pkg, version: "1.0.1" })),
|
|
216
|
+
} as unknown as ApplicationPlan;
|
|
217
|
+
expect(
|
|
218
|
+
await resolveDeploymentCompositionV1({ plan: bumped, composition }),
|
|
219
|
+
).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -18,9 +18,13 @@ import {
|
|
|
18
18
|
import type { ApplicationPlan } from "@frockbot/kernel-composition/compiler";
|
|
19
19
|
import {
|
|
20
20
|
bootstrapGeneration,
|
|
21
|
+
compositionArtifactSetHashV1,
|
|
22
|
+
compositionGenerationIdV1,
|
|
23
|
+
decodeCompositionGenerationV1,
|
|
21
24
|
type CompositionGenerationV1,
|
|
22
25
|
type CompositionHost,
|
|
23
26
|
type CompositionMemberV1,
|
|
27
|
+
type CompositionStore,
|
|
24
28
|
type MountedComposition,
|
|
25
29
|
} from "@frockbot/kernel-composition/generation";
|
|
26
30
|
import {
|
|
@@ -57,6 +61,92 @@ export function bootstrapCompositionGeneration(
|
|
|
57
61
|
);
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
/** The audit line a deployment-following generation carries. */
|
|
65
|
+
export const DEPLOYMENT_FOLLOW_SUMMARY_V1 =
|
|
66
|
+
"Updated the built-in Packages to this deployment's";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the deployment's built-in Packages into this Bot's next generation.
|
|
70
|
+
*
|
|
71
|
+
* A first-party member's manifest and artifact live in the compiled
|
|
72
|
+
* application, keyed by hash. A deploy that changes one of them (2026-09-05:
|
|
73
|
+
* the Applets list page) leaves every pinned generation naming a manifest this
|
|
74
|
+
* deployment no longer ships, and every Turn of every Bot fails to mount. A
|
|
75
|
+
* built-in member is the deployment's to update, never the Bot's, so before a
|
|
76
|
+
* Turn is admitted the Bot's first-party members are brought up to the
|
|
77
|
+
* application's, as a new generation with the old one as its parent. Members
|
|
78
|
+
* the Bot or its User put in are carried over exactly as they are; Applet
|
|
79
|
+
* members stay pinned; a deployment that changes nothing proposes nothing.
|
|
80
|
+
*
|
|
81
|
+
* Only the manifest and the artifact decide. A version string that moved with
|
|
82
|
+
* a release while both stayed the same mounts exactly as before, and a
|
|
83
|
+
* generation per Bot per release would eat the User's retention quota for
|
|
84
|
+
* nothing.
|
|
85
|
+
*/
|
|
86
|
+
export async function resolveDeploymentCompositionV1(options: {
|
|
87
|
+
plan: ApplicationPlan;
|
|
88
|
+
composition: Pick<CompositionStore, "current" | "propose">;
|
|
89
|
+
now?: Date;
|
|
90
|
+
}): Promise<CompositionGenerationV1 | undefined> {
|
|
91
|
+
const current = await options.composition.current();
|
|
92
|
+
const createdAt = (options.now ?? new Date()).toISOString();
|
|
93
|
+
const deployed = await bootstrapCompositionGeneration(
|
|
94
|
+
options.plan,
|
|
95
|
+
createdAt,
|
|
96
|
+
);
|
|
97
|
+
const shipped = new Map(
|
|
98
|
+
deployed.members.map((member) => [member.packageId, member]),
|
|
99
|
+
);
|
|
100
|
+
const members: CompositionMemberV1[] = [];
|
|
101
|
+
let changed = false;
|
|
102
|
+
for (const member of current.members) {
|
|
103
|
+
if (member.provenance.kind !== "first-party") {
|
|
104
|
+
members.push(member);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const replacement = shipped.get(member.packageId);
|
|
108
|
+
shipped.delete(member.packageId);
|
|
109
|
+
if (!replacement) {
|
|
110
|
+
// The deployment no longer ships it; nothing could mount it anyway.
|
|
111
|
+
changed = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (
|
|
115
|
+
replacement.manifestHash !== member.manifestHash ||
|
|
116
|
+
replacement.artifact?.contentHash !== member.artifact?.contentHash
|
|
117
|
+
) {
|
|
118
|
+
changed = true;
|
|
119
|
+
members.push(replacement);
|
|
120
|
+
} else {
|
|
121
|
+
members.push(member);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const added of shipped.values()) {
|
|
125
|
+
changed = true;
|
|
126
|
+
members.push(added);
|
|
127
|
+
}
|
|
128
|
+
if (!changed) return undefined;
|
|
129
|
+
const ordered = members.sort((left, right) =>
|
|
130
|
+
left.packageId.localeCompare(right.packageId),
|
|
131
|
+
);
|
|
132
|
+
const applets = current.applets ?? [];
|
|
133
|
+
const artifactSetHash = await compositionArtifactSetHashV1(ordered, applets);
|
|
134
|
+
const generation = decodeCompositionGenerationV1({
|
|
135
|
+
schemaVersion: 1,
|
|
136
|
+
generationId: compositionGenerationIdV1(createdAt, artifactSetHash),
|
|
137
|
+
artifactSetHash,
|
|
138
|
+
parentGenerationId: current.generationId,
|
|
139
|
+
summary: DEPLOYMENT_FOLLOW_SUMMARY_V1,
|
|
140
|
+
createdAt,
|
|
141
|
+
origin: { kind: "bootstrap" },
|
|
142
|
+
members: ordered,
|
|
143
|
+
...(applets.length === 0 ? {} : { applets }),
|
|
144
|
+
status: "pending",
|
|
145
|
+
});
|
|
146
|
+
await options.composition.propose(generation, { pin: true });
|
|
147
|
+
return generation;
|
|
148
|
+
}
|
|
149
|
+
|
|
60
150
|
export interface ShellMountedComposition extends MountedComposition {
|
|
61
151
|
readonly runtime: FoundationRuntime;
|
|
62
152
|
}
|
package/src/backend.ts
CHANGED
|
@@ -126,12 +126,14 @@ import {
|
|
|
126
126
|
planBotRunRecovery,
|
|
127
127
|
planInterruptedRunRecoveryV1,
|
|
128
128
|
} from "./backend-recovery.js";
|
|
129
|
+
import { COMPOSITION_CURRENT_KEY } from "@frockbot/kernel-do";
|
|
129
130
|
import {
|
|
130
131
|
bootstrapCompositionGeneration,
|
|
131
132
|
createShellCompositionHost,
|
|
132
133
|
type ShellAppletMountOptions,
|
|
133
134
|
type ShellIsolateMountOptions,
|
|
134
135
|
type ShellMountedComposition,
|
|
136
|
+
resolveDeploymentCompositionV1,
|
|
135
137
|
} from "./backend-composition.js";
|
|
136
138
|
import {
|
|
137
139
|
createAppletCapabilityHostV1,
|
|
@@ -1230,7 +1232,8 @@ export class ShellBotBackendContribution {
|
|
|
1230
1232
|
// from the previous Turn has already handed the log back (ADR 0030).
|
|
1231
1233
|
await yieldCompactionWorkV1(command.sessionId);
|
|
1232
1234
|
// Before admission, so the pin this Turn takes already carries whatever the
|
|
1233
|
-
// User's Applet directory says now.
|
|
1235
|
+
// deployment ships and whatever the User's Applet directory says now.
|
|
1236
|
+
await this.followDeploymentComposition();
|
|
1234
1237
|
await this.resolveAppletComposition(
|
|
1235
1238
|
{ userId: command.userId, botId: command.botId },
|
|
1236
1239
|
command,
|
|
@@ -1245,6 +1248,7 @@ export class ShellBotBackendContribution {
|
|
|
1245
1248
|
await this.validateIdentity(identity);
|
|
1246
1249
|
const catalog = await this.listPackageUi(identity);
|
|
1247
1250
|
const contribution = requirePackageUiToolDeclarationV1(catalog, command);
|
|
1251
|
+
await this.followDeploymentComposition();
|
|
1248
1252
|
return projectClientTurnV1(
|
|
1249
1253
|
await this.authority.run({
|
|
1250
1254
|
...identity,
|
|
@@ -2723,6 +2727,39 @@ export class ShellBotBackendContribution {
|
|
|
2723
2727
|
return focused;
|
|
2724
2728
|
}
|
|
2725
2729
|
|
|
2730
|
+
/**
|
|
2731
|
+
* Bring this Bot's built-in members up to the deployment before a Turn is
|
|
2732
|
+
* admitted, so a release that changed a first-party manifest or artifact is
|
|
2733
|
+
* a new generation rather than a Bot that cannot mount (2026-09-05, when
|
|
2734
|
+
* every Bot failed every Turn after the Applets list page changed). Outside
|
|
2735
|
+
* the admission transaction like the Applet resolve below; a failure here
|
|
2736
|
+
* leaves the Bot on the generation it has and is visible where that
|
|
2737
|
+
* generation fails to mount.
|
|
2738
|
+
*/
|
|
2739
|
+
private async followDeploymentComposition(): Promise<void> {
|
|
2740
|
+
try {
|
|
2741
|
+
// A Bot with nothing pinned yet bootstraps from this deployment when
|
|
2742
|
+
// its first Turn is admitted; reading the current generation here would
|
|
2743
|
+
// materialize that bootstrap early, and a refused command must leave
|
|
2744
|
+
// storage exactly as it found it.
|
|
2745
|
+
if (
|
|
2746
|
+
(await this.ctx.storage.get<unknown>(COMPOSITION_CURRENT_KEY)) ===
|
|
2747
|
+
undefined
|
|
2748
|
+
)
|
|
2749
|
+
return;
|
|
2750
|
+
await resolveDeploymentCompositionV1({
|
|
2751
|
+
plan: await this.compileApplication(),
|
|
2752
|
+
composition: {
|
|
2753
|
+
current: () => this.authority.composition.current(),
|
|
2754
|
+
propose: (generation, options) =>
|
|
2755
|
+
this.authority.composition.propose(generation, options),
|
|
2756
|
+
},
|
|
2757
|
+
});
|
|
2758
|
+
} catch {
|
|
2759
|
+
// The mount records why; never a wedged Turn on its own.
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2726
2763
|
/**
|
|
2727
2764
|
* Resolve the User's Applet directory into this Bot's next Composition
|
|
2728
2765
|
* generation, before a Turn is admitted.
|
|
@@ -3262,6 +3299,7 @@ export class ShellBotBackendContribution {
|
|
|
3262
3299
|
fire: RoutineFireV1,
|
|
3263
3300
|
): Promise<RoutineFireOutcomeV1> {
|
|
3264
3301
|
try {
|
|
3302
|
+
await this.followDeploymentComposition();
|
|
3265
3303
|
await this.authority.run(
|
|
3266
3304
|
routineTurnCommandV1(identity, fire, new Date().toISOString()),
|
|
3267
3305
|
);
|
|
@@ -4197,6 +4235,7 @@ export class ShellBotBackendContribution {
|
|
|
4197
4235
|
await this.ctx.storage.put(key, { ...context, status: "running" });
|
|
4198
4236
|
let outcome: TaskOutcomeV1;
|
|
4199
4237
|
try {
|
|
4238
|
+
await this.followDeploymentComposition();
|
|
4200
4239
|
await this.authority.run({
|
|
4201
4240
|
...identity,
|
|
4202
4241
|
runId: context.taskId,
|