@frockbot/computer-core 0.3.16 → 0.3.18
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 +2 -2
- package/src/core.ts +43 -2
- package/src/index.test.ts +65 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/computer-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
15
|
+
"@frockbot/kernel-contracts": "0.3.18",
|
|
16
16
|
"cordis": "4.0.0-rc.8"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
package/src/core.ts
CHANGED
|
@@ -663,7 +663,7 @@ export type ComputerSyncReasonV1 = "open" | "signal" | "turn-end" | "publish";
|
|
|
663
663
|
* answers `unavailable` and a Turn continues.
|
|
664
664
|
*/
|
|
665
665
|
export interface ComputerSyncSummaryV1 {
|
|
666
|
-
status: "ok" | "unavailable" | "refused" | "skipped";
|
|
666
|
+
status: "ok" | "degraded" | "unavailable" | "refused" | "skipped";
|
|
667
667
|
/** Human-readable reason, empty when the run had nothing to say. */
|
|
668
668
|
detail: string;
|
|
669
669
|
pulled: number;
|
|
@@ -671,6 +671,10 @@ export interface ComputerSyncSummaryV1 {
|
|
|
671
671
|
restored: number;
|
|
672
672
|
removed: number;
|
|
673
673
|
adopted: number;
|
|
674
|
+
/** Reproducible directories and durable entries excluded by policy. */
|
|
675
|
+
ignored: number;
|
|
676
|
+
/** Manifest entries left out after the scan reached its hard bound. */
|
|
677
|
+
omitted: number;
|
|
674
678
|
conflicts: number;
|
|
675
679
|
failures: number;
|
|
676
680
|
}
|
|
@@ -687,6 +691,8 @@ export function computerSyncSummaryV1(
|
|
|
687
691
|
restored: 0,
|
|
688
692
|
removed: 0,
|
|
689
693
|
adopted: 0,
|
|
694
|
+
ignored: 0,
|
|
695
|
+
omitted: 0,
|
|
690
696
|
conflicts: 0,
|
|
691
697
|
failures: 0,
|
|
692
698
|
};
|
|
@@ -720,7 +726,7 @@ export interface ComputerSyncV1 {
|
|
|
720
726
|
reconcileRoot?(
|
|
721
727
|
root: WorkspaceRootV1,
|
|
722
728
|
reason: ComputerSyncReasonV1,
|
|
723
|
-
options?:
|
|
729
|
+
options?: ComputerRootSyncOptionsV1,
|
|
724
730
|
): Promise<ComputerSyncSummaryV1>;
|
|
725
731
|
/**
|
|
726
732
|
* The Computer-side watcher's change signal, or `undefined` when it cannot
|
|
@@ -730,6 +736,18 @@ export interface ComputerSyncV1 {
|
|
|
730
736
|
signal(options?: ComputerOperationOptions): Promise<string | undefined>;
|
|
731
737
|
}
|
|
732
738
|
|
|
739
|
+
/** Options for the one-root reconciliation used by artifact publishers. */
|
|
740
|
+
export interface ComputerRootSyncOptionsV1 extends ComputerOperationOptions {
|
|
741
|
+
/**
|
|
742
|
+
* Exact root-relative files that are required even when they live below a
|
|
743
|
+
* reproducible directory the ordinary Workspace sync excludes.
|
|
744
|
+
*
|
|
745
|
+
* This is a path selection, not a Package exception: the Computer provider
|
|
746
|
+
* does not learn which caller or Package needs the bytes.
|
|
747
|
+
*/
|
|
748
|
+
requiredPaths?: readonly string[];
|
|
749
|
+
}
|
|
750
|
+
|
|
733
751
|
/**
|
|
734
752
|
* What a host supplies so a Computer Package can build the sync: the
|
|
735
753
|
* object-storage side of the durable roots, and the Durable Object records the
|
|
@@ -877,6 +895,29 @@ function guardedHandle(
|
|
|
877
895
|
guardedOperation(assertCurrent, () =>
|
|
878
896
|
sync.reconcile(reason, options),
|
|
879
897
|
),
|
|
898
|
+
// Forwarded when the provider offers it. Dropping it here is how a
|
|
899
|
+
// publish's required `dist/` pull was "refused: this Computer cannot
|
|
900
|
+
// reconcile a single durable root" on production (Bob, 2026-09-04)
|
|
901
|
+
// while the provider underneath could — and once ordinary sync
|
|
902
|
+
// stopped carrying `dist/`, that refusal was the whole failure.
|
|
903
|
+
...(sync.reconcileRoot
|
|
904
|
+
? {
|
|
905
|
+
reconcileRoot: (
|
|
906
|
+
root: Parameters<
|
|
907
|
+
NonNullable<ComputerSyncV1["reconcileRoot"]>
|
|
908
|
+
>[0],
|
|
909
|
+
reason: Parameters<
|
|
910
|
+
NonNullable<ComputerSyncV1["reconcileRoot"]>
|
|
911
|
+
>[1],
|
|
912
|
+
options?: Parameters<
|
|
913
|
+
NonNullable<ComputerSyncV1["reconcileRoot"]>
|
|
914
|
+
>[2],
|
|
915
|
+
) =>
|
|
916
|
+
guardedOperation(assertCurrent, () =>
|
|
917
|
+
sync.reconcileRoot!(root, reason, options),
|
|
918
|
+
),
|
|
919
|
+
}
|
|
920
|
+
: {}),
|
|
880
921
|
signal: (options) =>
|
|
881
922
|
guardedOperation(assertCurrent, () => sync.signal(options)),
|
|
882
923
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -230,6 +230,71 @@ describe("ComputerRegistry", () => {
|
|
|
230
230
|
await root.fiber.dispose();
|
|
231
231
|
});
|
|
232
232
|
|
|
233
|
+
test("keeps a provider's single-root reconciliation on the guarded handle", async () => {
|
|
234
|
+
// Bob on production (2026-09-04): the provider could reconcile one root,
|
|
235
|
+
// the guarded handle dropped that method, and the publish that needed
|
|
236
|
+
// `dist/` pulled explicitly was refused as "cannot reconcile a single
|
|
237
|
+
// durable root" while ordinary sync no longer carried build output.
|
|
238
|
+
const root = new Context();
|
|
239
|
+
await root.plugin(ComputerRegistry);
|
|
240
|
+
const calls: string[] = [];
|
|
241
|
+
const syncing: ComputerProvider = {
|
|
242
|
+
id: "sprites",
|
|
243
|
+
open: async (identity, tenant, assignment) => ({
|
|
244
|
+
assignment,
|
|
245
|
+
identity,
|
|
246
|
+
tenant,
|
|
247
|
+
sync: {
|
|
248
|
+
reconcile: async (reason) => {
|
|
249
|
+
calls.push(`reconcile:${reason}`);
|
|
250
|
+
return { status: "ok" } as never;
|
|
251
|
+
},
|
|
252
|
+
reconcileRoot: async (declared, reason, options) => {
|
|
253
|
+
calls.push(
|
|
254
|
+
`root:${reason}:${declared.kind}:${(options?.requiredPaths ?? []).join(",")}`,
|
|
255
|
+
);
|
|
256
|
+
return { status: "ok" } as never;
|
|
257
|
+
},
|
|
258
|
+
signal: async () => undefined,
|
|
259
|
+
},
|
|
260
|
+
close: () => Promise.resolve(),
|
|
261
|
+
}),
|
|
262
|
+
};
|
|
263
|
+
root.computers.register(syncing);
|
|
264
|
+
const identity = { userId: "user-1" };
|
|
265
|
+
root.computers.assign(identity, "sprites");
|
|
266
|
+
const computer = await root.computers.open(identity, { botId: "bot-1" });
|
|
267
|
+
|
|
268
|
+
expect(computer.sync?.reconcileRoot).toBeDefined();
|
|
269
|
+
await computer.sync?.reconcileRoot?.(
|
|
270
|
+
{
|
|
271
|
+
kind: "package-declared",
|
|
272
|
+
userId: "user-1",
|
|
273
|
+
packageId: "applets",
|
|
274
|
+
rootId: "source",
|
|
275
|
+
},
|
|
276
|
+
"publish",
|
|
277
|
+
{ requiredPaths: ["a.b/dist/server.js"] },
|
|
278
|
+
);
|
|
279
|
+
expect(calls).toEqual(["root:publish:package-declared:a.b/dist/server.js"]);
|
|
280
|
+
|
|
281
|
+
// Still guarded: a stale handle refuses it like every other operation.
|
|
282
|
+
root.computers.register({ ...syncing, id: "local" });
|
|
283
|
+
root.computers.assign(identity, "local");
|
|
284
|
+
await expect(
|
|
285
|
+
computer.sync?.reconcileRoot?.(
|
|
286
|
+
{
|
|
287
|
+
kind: "package-declared",
|
|
288
|
+
userId: "user-1",
|
|
289
|
+
packageId: "applets",
|
|
290
|
+
rootId: "source",
|
|
291
|
+
},
|
|
292
|
+
"publish",
|
|
293
|
+
),
|
|
294
|
+
).rejects.toMatchObject({ code: "stale-assignment" });
|
|
295
|
+
await root.fiber.dispose();
|
|
296
|
+
});
|
|
297
|
+
|
|
233
298
|
test("fails clearly when a User has no Computer assignment", async () => {
|
|
234
299
|
const root = new Context();
|
|
235
300
|
await root.plugin(ComputerRegistry);
|