@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/computer-core",
3
- "version": "0.3.16",
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.16",
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?: ComputerOperationOptions,
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);