@fungi.computer/computer 0.0.1 → 0.2.0
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/README.md +38 -10
- package/dist/computer-backends.d.ts +11 -12
- package/dist/computer-backends.js +4 -7
- package/dist/exec-output.d.ts +13 -0
- package/dist/exec-output.js +24 -0
- package/dist/execution-lifecycle.d.ts +35 -0
- package/dist/execution-lifecycle.js +78 -0
- package/dist/files-patch.d.ts +12 -4
- package/dist/files-patch.js +8 -14
- package/dist/files.d.ts +65 -15
- package/dist/files.js +152 -31
- package/dist/fleet/backing.js +2 -2
- package/dist/fleet/index.d.ts +1 -1
- package/dist/fleet/index.js +1 -1
- package/dist/fleet/inventory-wire.d.ts +4 -0
- package/dist/fleet/inventory-wire.js +5 -0
- package/dist/fleet/inventory.js +4 -4
- package/dist/fleet/lifecycle.d.ts +7 -1
- package/dist/fleet/lifecycle.js +23 -6
- package/dist/fleet/wire.d.ts +28 -28
- package/dist/fleet/wire.js +9 -9
- package/dist/index.d.ts +33 -15
- package/dist/index.js +426 -109
- package/dist/internet-access.d.ts +4 -0
- package/dist/internet-access.js +14 -0
- package/dist/local-backend-replay.d.ts +10 -0
- package/dist/local-backend-replay.js +43 -0
- package/dist/local-backend.d.ts +2 -1
- package/dist/local-backend.js +4 -4
- package/dist/on-box-vm.d.ts +15 -4
- package/dist/on-box-vm.js +43 -20
- package/dist/operation-record.d.ts +1 -1
- package/dist/operation-record.js +1 -1
- package/dist/process-inventory.d.ts +3 -1
- package/dist/process-inventory.js +2 -2
- package/dist/team-candidate-builder.mjs +8428 -337
- package/package.json +17 -24
- package/dist/light-shell.d.ts +0 -44
- package/dist/light-shell.js +0 -376
- package/dist/operation-contract.d.ts +0 -456
- package/dist/operation-contract.js +0 -452
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
2
|
+
/** This Computer's durable public-internet switch. Absence means on.
|
|
3
|
+
* Global and Team policy still override it at the egress gateway. */
|
|
4
|
+
export declare function computerInternetAccess(sql: WatchdogSqliteOwner["sql"], enabled?: boolean): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** This Computer's durable public-internet switch. Absence means on.
|
|
3
|
+
* Global and Team policy still override it at the egress gateway. */
|
|
4
|
+
export function computerInternetAccess(sql, enabled) {
|
|
5
|
+
sql.exec("CREATE TABLE IF NOT EXISTS computer_internet_access (singleton INTEGER PRIMARY KEY CHECK (singleton=1), enabled INTEGER NOT NULL CHECK (enabled IN (0,1))) STRICT");
|
|
6
|
+
if (enabled !== undefined)
|
|
7
|
+
sql.exec("INSERT INTO computer_internet_access (singleton,enabled) VALUES (1,?) ON CONFLICT (singleton) DO UPDATE SET enabled=excluded.enabled", enabled ? 1 : 0);
|
|
8
|
+
const row = sql
|
|
9
|
+
.exec("SELECT enabled FROM computer_internet_access WHERE singleton=1")
|
|
10
|
+
.toArray()[0];
|
|
11
|
+
return row === undefined
|
|
12
|
+
? true
|
|
13
|
+
: z.union([z.literal(0), z.literal(1)]).parse(row.enabled) === 1;
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SeamReplayStore } from "@fungi.computer/seam";
|
|
2
|
+
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
3
|
+
/** Private receiver-local storage ceiling, not a Team or attachment quota. */
|
|
4
|
+
export declare const maximumLocalDialReplayRecords = 4096;
|
|
5
|
+
/**
|
|
6
|
+
* One Computer's local-dial receiver history, in its existing owner database.
|
|
7
|
+
* Reconstruct with the same owner, never a process-generation namespace.
|
|
8
|
+
* Native SQLite owns atomicity and commit; there is no in-memory fallback.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createLocalBackendReplayStore(owner: WatchdogSqliteOwner): SeamReplayStore;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Private receiver-local storage ceiling, not a Team or attachment quota. */
|
|
3
|
+
export const maximumLocalDialReplayRecords = 4096;
|
|
4
|
+
const countSchema = z.number().int().nonnegative();
|
|
5
|
+
/**
|
|
6
|
+
* One Computer's local-dial receiver history, in its existing owner database.
|
|
7
|
+
* Reconstruct with the same owner, never a process-generation namespace.
|
|
8
|
+
* Native SQLite owns atomicity and commit; there is no in-memory fallback.
|
|
9
|
+
*/
|
|
10
|
+
export function createLocalBackendReplayStore(owner) {
|
|
11
|
+
return {
|
|
12
|
+
admit({ nonce, now, expiresAt }) {
|
|
13
|
+
try {
|
|
14
|
+
return owner.transactionSync(() => {
|
|
15
|
+
owner.sql
|
|
16
|
+
.exec(`CREATE TABLE IF NOT EXISTS computer_local_dial_replay (
|
|
17
|
+
nonce TEXT PRIMARY KEY,
|
|
18
|
+
expires_at INTEGER NOT NULL
|
|
19
|
+
) STRICT`);
|
|
20
|
+
// Seam's last fresh instant is inclusive. Do not renew duplicate
|
|
21
|
+
// deadlines, assume expiry order, or discard any still-live record.
|
|
22
|
+
owner.sql.exec("DELETE FROM computer_local_dial_replay WHERE expires_at < ?", now);
|
|
23
|
+
if (owner.sql
|
|
24
|
+
.exec("SELECT nonce FROM computer_local_dial_replay WHERE nonce = ?", nonce)
|
|
25
|
+
.toArray().length > 0)
|
|
26
|
+
return "replayed";
|
|
27
|
+
const count = countSchema.parse(owner.sql
|
|
28
|
+
.exec("SELECT COUNT(*) AS count FROM computer_local_dial_replay")
|
|
29
|
+
.toArray()[0]?.count);
|
|
30
|
+
if (count >= maximumLocalDialReplayRecords)
|
|
31
|
+
return "at-capacity";
|
|
32
|
+
owner.sql.exec("INSERT INTO computer_local_dial_replay (nonce, expires_at) VALUES (?, ?)", nonce, expiresAt);
|
|
33
|
+
return "admitted";
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Covers schema, decoding, statement and commit failures. SQLite rolls
|
|
38
|
+
// back a failed transaction; no successful admission escapes it.
|
|
39
|
+
return "unavailable";
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/local-backend.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BackendHandle, WorkspaceBackend } from "@fungi.computer/cloudflare-computer";
|
|
2
|
+
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
2
3
|
import { type LocalBackend } from "./computer-backends.js";
|
|
3
4
|
/**
|
|
4
5
|
* A member's machine as a Workspace backend.
|
|
@@ -44,7 +45,7 @@ export declare function localBackendAudience(computerName: string, backendId: st
|
|
|
44
45
|
* pinned. The WebSocket upgrade carries no body, so it signs an unsigned
|
|
45
46
|
* payload over the exact path.
|
|
46
47
|
*/
|
|
47
|
-
export declare function verifyLocalBackendDial(request: Request, computerName: string, backend: LocalBackend): Promise<boolean>;
|
|
48
|
+
export declare function verifyLocalBackendDial(request: Request, computerName: string, backend: LocalBackend, owner: WatchdogSqliteOwner): Promise<boolean>;
|
|
48
49
|
/**
|
|
49
50
|
* The public dial path: `/api/teams/:team/computers/:computer/backends/:id/connect`.
|
|
50
51
|
* The Hub forwards it unchanged so the signature covers what the machine sent.
|
package/dist/local-backend.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { workspaceStream } from "./workspace-stream.js";
|
|
2
2
|
import { newWebSocketRpcSession } from "@fungi.computer/capnweb";
|
|
3
|
-
import {
|
|
3
|
+
import { importSeamVerifiers, seamHeader, verifySeamRequest, } from "@fungi.computer/seam";
|
|
4
4
|
import { attachedBackendId } from "./computer-backends.js";
|
|
5
|
+
import { createLocalBackendReplayStore } from "./local-backend-replay.js";
|
|
5
6
|
/**
|
|
6
7
|
* A member's machine as a Workspace backend.
|
|
7
8
|
*
|
|
@@ -120,15 +121,14 @@ export class LocalWorkspaceBackend {
|
|
|
120
121
|
export function localBackendAudience(computerName, backendId) {
|
|
121
122
|
return `fungi-local-backend:${computerName}:${backendId}`;
|
|
122
123
|
}
|
|
123
|
-
const dialReplay = new SeamReplayGuard();
|
|
124
124
|
/**
|
|
125
125
|
* Verify a machine's signed dial-in against the public key its attachment
|
|
126
126
|
* pinned. The WebSocket upgrade carries no body, so it signs an unsigned
|
|
127
127
|
* payload over the exact path.
|
|
128
128
|
*/
|
|
129
|
-
export async function verifyLocalBackendDial(request, computerName, backend) {
|
|
129
|
+
export async function verifyLocalBackendDial(request, computerName, backend, owner) {
|
|
130
130
|
try {
|
|
131
|
-
await verifySeamRequest(await importSeamVerifiers({ [backend.deviceId]: backend.publicKey }), localBackendAudience(computerName, attachedBackendId(backend)), request.method, request.url, request.headers.get(seamHeader), null,
|
|
131
|
+
await verifySeamRequest(await importSeamVerifiers({ [backend.deviceId]: backend.publicKey }), localBackendAudience(computerName, attachedBackendId(backend)), request.method, request.url, request.headers.get(seamHeader), null, createLocalBackendReplayStore(owner));
|
|
132
132
|
return true;
|
|
133
133
|
}
|
|
134
134
|
catch (cause) {
|
package/dist/on-box-vm.d.ts
CHANGED
|
@@ -42,24 +42,35 @@ export declare function onBoxVmPlacement(env: OnBoxVmEnv, hostId: string | undef
|
|
|
42
42
|
declare const vmStateSchema: z.ZodObject<{
|
|
43
43
|
running: z.ZodBoolean;
|
|
44
44
|
runtimeId: z.ZodNullable<z.ZodString>;
|
|
45
|
-
exit: z.ZodNullable<z.ZodObject<{
|
|
45
|
+
exit: z.ZodNullable<z.ZodLazy<z.ZodObject<{
|
|
46
46
|
reason: z.ZodString;
|
|
47
47
|
exitedAt: z.ZodNumber;
|
|
48
|
-
}, z.core.$strict
|
|
48
|
+
}, z.core.$strict>>>;
|
|
49
49
|
}, z.core.$strict>;
|
|
50
50
|
export type OnBoxVmState = z.infer<typeof vmStateSchema>;
|
|
51
51
|
declare const vmInventorySchema: z.ZodArray<z.ZodObject<{
|
|
52
52
|
running: z.ZodBoolean;
|
|
53
53
|
runtimeId: z.ZodNullable<z.ZodString>;
|
|
54
|
-
exit: z.ZodNullable<z.ZodObject<{
|
|
54
|
+
exit: z.ZodNullable<z.ZodLazy<z.ZodObject<{
|
|
55
55
|
reason: z.ZodString;
|
|
56
56
|
exitedAt: z.ZodNumber;
|
|
57
|
-
}, z.core.$strict
|
|
57
|
+
}, z.core.$strict>>>;
|
|
58
58
|
id: z.ZodString;
|
|
59
59
|
name: z.ZodString;
|
|
60
60
|
lastActive: z.ZodNumber;
|
|
61
61
|
}, z.core.$strict>>;
|
|
62
62
|
export type OnBoxVmInventory = z.infer<typeof vmInventorySchema>;
|
|
63
|
+
export type OnBoxVmOperation = "status" | "start" | "restart" | "destroy" | "intercept" | "inventory";
|
|
64
|
+
declare const OnBoxVmTimeoutError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
65
|
+
readonly _tag: "OnBoxVmTimeoutError";
|
|
66
|
+
} & Readonly<A>;
|
|
67
|
+
/** The host's outcome is unknown after a timeout; this is not evidence of offline. */
|
|
68
|
+
export declare class OnBoxVmTimeoutError extends OnBoxVmTimeoutError_base<{
|
|
69
|
+
readonly operation: OnBoxVmOperation;
|
|
70
|
+
readonly timeoutMs: number;
|
|
71
|
+
}> {
|
|
72
|
+
get message(): string;
|
|
73
|
+
}
|
|
63
74
|
/** One Durable Object's view of its microVM through the host service. */
|
|
64
75
|
export declare class OnBoxVmContainerHost implements IWorkspaceContainerAPI {
|
|
65
76
|
#private;
|
package/dist/on-box-vm.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import * as Data from "effect/Data";
|
|
2
3
|
import { SeamReplayGuard, importSeamSigner, importSeamVerifiers, seamBodyDigest, seamHeader, signSeamRequest, unsignedPayload, verifySeamRequest, } from "@fungi.computer/seam";
|
|
3
4
|
function accessHeaders(env) {
|
|
4
5
|
const id = env.COMPUTER_VM_ACCESS_CLIENT_ID;
|
|
@@ -13,13 +14,13 @@ function accessHeaders(env) {
|
|
|
13
14
|
"CF-Access-Client-Secret": pair.data[1],
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
|
-
const hostIdSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/);
|
|
17
|
-
const hostRegistrySchema = z.record(hostIdSchema, z.strictObject({
|
|
17
|
+
const hostIdSchema = z.lazy(() => z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/));
|
|
18
|
+
const hostRegistrySchema = z.lazy(() => z.record(hostIdSchema, z.strictObject({
|
|
18
19
|
origin: z
|
|
19
20
|
.url({ protocol: /^https?$/ })
|
|
20
21
|
.transform((value) => new URL(value).origin),
|
|
21
22
|
publicKey: z.base64().length(44),
|
|
22
|
-
}));
|
|
23
|
+
})));
|
|
23
24
|
const registries = new Map();
|
|
24
25
|
function hostRegistry(env) {
|
|
25
26
|
const raw = env.COMPUTER_VM_HOSTS ?? "{}";
|
|
@@ -72,10 +73,10 @@ function hostVerifiers(env) {
|
|
|
72
73
|
const vmHostAudience = (hostId) => `computer-vm-host:${hostId}`;
|
|
73
74
|
/** The audience this Worker's ingress door verifies. */
|
|
74
75
|
const vmIngressAudience = "computer-vm-ingress";
|
|
75
|
-
const exitSchema = z.strictObject({
|
|
76
|
+
const exitSchema = z.lazy(() => z.strictObject({
|
|
76
77
|
reason: z.string().max(1_024),
|
|
77
78
|
exitedAt: z.number().int().nonnegative(),
|
|
78
|
-
});
|
|
79
|
+
}));
|
|
79
80
|
const vmStateSchema = z.strictObject({
|
|
80
81
|
running: z.boolean(),
|
|
81
82
|
runtimeId: z.string().min(1).max(128).nullable(),
|
|
@@ -86,9 +87,17 @@ const vmInventorySchema = z.array(vmStateSchema.extend({
|
|
|
86
87
|
name: z.string(),
|
|
87
88
|
lastActive: z.number().int().nonnegative(),
|
|
88
89
|
}));
|
|
89
|
-
const vmStartSchema = z.strictObject({
|
|
90
|
+
const vmStartSchema = z.lazy(() => z.strictObject({
|
|
90
91
|
runtimeId: z.string().min(1).max(128),
|
|
91
|
-
});
|
|
92
|
+
}));
|
|
93
|
+
/** A bounded control-plane wait, not the VM's execution or allocation lifetime. */
|
|
94
|
+
const vmControlTimeoutMs = 60_000;
|
|
95
|
+
/** The host's outcome is unknown after a timeout; this is not evidence of offline. */
|
|
96
|
+
export class OnBoxVmTimeoutError extends Data.TaggedError("OnBoxVmTimeoutError") {
|
|
97
|
+
get message() {
|
|
98
|
+
return `On-box Computer VM ${this.operation} timed out after ${String(this.timeoutMs)} ms; host outcome unknown`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
92
101
|
function vmBodyBytes(body) {
|
|
93
102
|
return new Uint8Array(new TextEncoder().encode(body === undefined ? "" : JSON.stringify(body)));
|
|
94
103
|
}
|
|
@@ -121,14 +130,32 @@ export class OnBoxVmContainerHost {
|
|
|
121
130
|
this.#sleepAfterMs = sleepAfterMs;
|
|
122
131
|
}
|
|
123
132
|
async #call(operation, schema, body) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
let path = `/vms/${this.#vmId}`;
|
|
134
|
+
if (operation === "inventory")
|
|
135
|
+
path = "/vms";
|
|
136
|
+
else if (operation !== "status")
|
|
137
|
+
path += `/${operation}`;
|
|
138
|
+
const url = `${this.#placement.origin}${path}`;
|
|
139
|
+
const signal = AbortSignal.timeout(vmControlTimeoutMs);
|
|
140
|
+
try {
|
|
141
|
+
const response = await fetch(url, {
|
|
142
|
+
...(await signedVmRequest(this.#env, this.#placement, url, body)),
|
|
143
|
+
signal,
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
const detail = (await response.text()).slice(0, 200);
|
|
147
|
+
throw new OnBoxVmUnavailableError(operation, response.status, detail);
|
|
148
|
+
}
|
|
149
|
+
return schema.parse(await response.json());
|
|
150
|
+
}
|
|
151
|
+
catch (cause) {
|
|
152
|
+
if (signal.aborted)
|
|
153
|
+
throw new OnBoxVmTimeoutError({
|
|
154
|
+
operation,
|
|
155
|
+
timeoutMs: vmControlTimeoutMs,
|
|
156
|
+
});
|
|
157
|
+
throw cause;
|
|
130
158
|
}
|
|
131
|
-
return schema.parse(await response.json());
|
|
132
159
|
}
|
|
133
160
|
async #boot(operation, env, enableInternet) {
|
|
134
161
|
const started = await this.#call(operation, vmStartSchema, {
|
|
@@ -139,12 +166,8 @@ export class OnBoxVmContainerHost {
|
|
|
139
166
|
return { runtimeId: started.runtimeId };
|
|
140
167
|
}
|
|
141
168
|
/** Signed host-wide inventory; includes VMs outside this Hub's directory. */
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const response = await fetch(url, await signedVmRequest(this.#env, this.#placement, url, undefined));
|
|
145
|
-
if (!response.ok)
|
|
146
|
-
throw new OnBoxVmUnavailableError("inventory", response.status);
|
|
147
|
-
return vmInventorySchema.parse(await response.json());
|
|
169
|
+
inventory() {
|
|
170
|
+
return this.#call("inventory", vmInventorySchema);
|
|
148
171
|
}
|
|
149
172
|
start(env, enableInternet) {
|
|
150
173
|
return this.#boot("start", env, enableInternet);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ComputerExecOutput, type ComputerOperationResult, type ComputerRequestId } from "
|
|
1
|
+
import { type ComputerExecOutput, type ComputerOperationResult, type ComputerRequestId } from "@fungi.computer/computer-contract";
|
|
2
2
|
/** Terminal status records are retained for 7 days. */
|
|
3
3
|
export declare const operationRetentionMs: number;
|
|
4
4
|
/** Command output may contain secrets, so it is cleared after 24 hours. */
|
package/dist/operation-record.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computerOperationResultSchema, computerRequestIdSchema, execOutputSchema, } from "
|
|
1
|
+
import { computerOperationResultSchema, computerRequestIdSchema, execOutputSchema, } from "@fungi.computer/computer-contract";
|
|
2
2
|
/** Terminal status records are retained for 7 days. */
|
|
3
3
|
export const operationRetentionMs = 7 * 24 * 60 * 60 * 1_000;
|
|
4
4
|
/** Command output may contain secrets, so it is cleared after 24 hours. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WorkspaceRuntimeEvent, WorkspaceRuntimeExecHandle } from "@fungi.computer/cloudflare-computer";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { processStartInputSchema, type ComputerProcess, type ComputerProcessId, type ComputerProcessSync } from "
|
|
3
|
+
import { processStartInputSchema, type ComputerProcess, type ComputerProcessId, type ComputerProcessSync } from "@fungi.computer/computer-contract";
|
|
4
4
|
/** The durable SQLite surface owned by processes and their port exposures. */
|
|
5
5
|
export type ComputerProcessStorage = Pick<DurableObjectStorage, "sql" | "transactionSync">;
|
|
6
6
|
/** Computer-owned replay identities; computerd owns execution and exit observations. */
|
|
@@ -54,6 +54,8 @@ export type ProcessRuntime = Readonly<{
|
|
|
54
54
|
}>;
|
|
55
55
|
/** How long a tail read may stay silent before the process counts as live. */
|
|
56
56
|
export declare const liveProbeMs = 750;
|
|
57
|
+
/** Classify a failed attach: busy is live, lost is gone, anything else is unknown. */
|
|
58
|
+
export declare function failedObservation(error: Error | undefined): ProcessObservation;
|
|
57
59
|
/**
|
|
58
60
|
* How long a drain may keep waiting for EOF once an exit has already been
|
|
59
61
|
* observed, before giving up: a backgrounded child (for example
|
|
@@ -51,7 +51,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
53
|
import { z } from "zod";
|
|
54
|
-
import { computerProcessSchema, processStartInputSchema, mintComputerProcessId, ComputerProcessLimitError, ComputerPortReservedError, ComputerPortConflictError, ComputerPortLimitError, isComputerReservedPort, maximumExposedPorts, maximumProcessRecords, maximumRunningProcesses, } from "
|
|
54
|
+
import { computerProcessSchema, processStartInputSchema, mintComputerProcessId, ComputerProcessLimitError, ComputerPortReservedError, ComputerPortConflictError, ComputerPortLimitError, isComputerReservedPort, maximumExposedPorts, maximumProcessRecords, maximumRunningProcesses, } from "@fungi.computer/computer-contract";
|
|
55
55
|
import { ComputerOperationConflictError } from "./operation-record.js";
|
|
56
56
|
function toProcess(row) {
|
|
57
57
|
return computerProcessSchema.parse({
|
|
@@ -211,7 +211,7 @@ function lostExecution(error) {
|
|
|
211
211
|
return lostCodeSchema.safeParse(error).success;
|
|
212
212
|
}
|
|
213
213
|
/** Classify a failed attach: busy is live, lost is gone, anything else is unknown. */
|
|
214
|
-
function failedObservation(error) {
|
|
214
|
+
export function failedObservation(error) {
|
|
215
215
|
if (error === undefined)
|
|
216
216
|
return { state: "uncertain" };
|
|
217
217
|
// computerd admits one live subscriber; the Computer's own watcher holding
|