@fungi.computer/truffle 0.1.1 → 0.2.1
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 +282 -34
- package/dist/auth-cli.d.ts +10 -0
- package/dist/auth-cli.js +36 -14
- package/dist/fungi-account-command.d.ts +1 -1
- package/dist/fungi-account-command.js +6 -3
- package/dist/index.d.ts +10 -2
- package/dist/index.js +8 -2
- package/dist/internal/image-fit-metafile.json +106 -0
- package/dist/internal/local-image-fit-worker.mjs +960 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +11 -12
- package/dist/internal/sandbox-metafile.json +7 -7
- package/dist/local-archive-handoff.d.ts +12 -0
- package/dist/local-archive-handoff.js +21 -0
- package/dist/local-extensions.d.ts +58 -12
- package/dist/local-extensions.js +195 -55
- package/dist/local-image-fit.d.ts +30 -0
- package/dist/local-image-fit.js +105 -0
- package/dist/local-models.d.ts +4 -2
- package/dist/local-models.js +11 -6
- package/dist/local-session-archives.d.ts +8 -0
- package/dist/local-session-archives.js +152 -0
- package/dist/local-skill-source.d.ts +18 -3
- package/dist/local-skill-source.js +216 -140
- package/dist/log-event.d.ts +7 -0
- package/dist/log-event.js +8 -4
- package/dist/private-bounded-response.d.ts +2 -0
- package/dist/private-bounded-response.js +41 -0
- package/dist/private-file-credential-store.d.ts +28 -3
- package/dist/private-file-credential-store.js +57 -9
- package/dist/private-files-patch.d.ts +2 -1
- package/dist/private-files-patch.js +21 -3
- package/dist/private-fungi-account.d.ts +40 -23
- package/dist/private-fungi-account.js +201 -21
- package/dist/private-fungi-mcp-connection.d.ts +4 -0
- package/dist/private-fungi-mcp-connection.js +25 -6
- package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
- package/dist/private-fungi-mcp-selection-schema.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +6 -6
- package/dist/private-fungi-mcp-selection.js +29 -16
- package/dist/private-fungi-model-api.js +4 -4
- package/dist/private-fungi-model-catalog.d.ts +10 -0
- package/dist/private-fungi-model-catalog.js +10 -2
- package/dist/private-fungi-model-provider.d.ts +1 -1
- package/dist/private-fungi-model-provider.js +6 -1
- package/dist/private-fungi-pending-logouts.d.ts +1 -1
- package/dist/private-fungi-pending-logouts.js +1 -2
- package/dist/private-fungi-service-logout.d.ts +44 -0
- package/dist/private-fungi-service-logout.js +59 -0
- package/dist/private-invocation-computer.d.ts +24 -3
- package/dist/private-invocation-computer.js +101 -62
- package/dist/private-invocation-files.d.ts +16 -1
- package/dist/private-invocation-files.js +154 -83
- package/dist/private-local-acp-stdio.js +3 -0
- package/dist/private-local-admission-gate.d.ts +14 -0
- package/dist/private-local-admission-gate.js +52 -0
- package/dist/private-local-agent-control.d.ts +101 -0
- package/dist/private-local-agent-control.js +30 -0
- package/dist/private-local-agent-default-model.d.ts +12 -0
- package/dist/private-local-agent-default-model.js +31 -0
- package/dist/private-local-auth-rpc.d.ts +67 -7
- package/dist/private-local-auth-rpc.js +140 -17
- package/dist/private-local-bwrap.d.ts +13 -0
- package/dist/private-local-bwrap.js +70 -0
- package/dist/private-local-client-version.d.ts +5 -0
- package/dist/private-local-client-version.js +43 -0
- package/dist/private-local-computer-attach.d.ts +3 -3
- package/dist/private-local-computer-attach.js +17 -3
- package/dist/private-local-computer-processes.d.ts +6 -3
- package/dist/private-local-computer-processes.js +55 -31
- package/dist/private-local-config-root.d.ts +21 -0
- package/dist/private-local-config-root.js +19 -0
- package/dist/private-local-containment.d.ts +51 -0
- package/dist/private-local-containment.js +161 -0
- package/dist/private-local-delegation.d.ts +10 -3
- package/dist/private-local-delegation.js +70 -15
- package/dist/private-local-effective-model.d.ts +9 -1
- package/dist/private-local-effective-model.js +3 -0
- package/dist/private-local-exec-environment.d.ts +16 -0
- package/dist/private-local-exec-environment.js +11 -0
- package/dist/private-local-linux-sandbox.d.ts +8 -0
- package/dist/private-local-linux-sandbox.js +67 -0
- package/dist/private-local-mail.d.ts +4 -2
- package/dist/private-local-mail.js +41 -33
- package/dist/private-local-mesh-enrollment.d.ts +34 -0
- package/dist/private-local-mesh-enrollment.js +188 -0
- package/dist/private-local-mesh-links.d.ts +21 -0
- package/dist/private-local-mesh-links.js +229 -0
- package/dist/private-local-mesh-record.d.ts +120 -0
- package/dist/private-local-mesh-record.js +55 -0
- package/dist/private-local-process-identity.d.ts +20 -0
- package/dist/private-local-process-identity.js +40 -0
- package/dist/private-local-runtime-host.d.ts +64 -13
- package/dist/private-local-runtime-host.js +361 -172
- package/dist/private-local-sandbox-policy.d.ts +21 -0
- package/dist/private-local-sandbox-policy.js +121 -0
- package/dist/private-local-sandbox.d.ts +37 -0
- package/dist/private-local-sandbox.js +114 -0
- package/dist/private-local-seatbelt.d.ts +24 -0
- package/dist/private-local-seatbelt.js +68 -0
- package/dist/private-local-service-contract.d.ts +91 -11
- package/dist/private-local-service-discovery.d.ts +129 -13
- package/dist/private-local-service-discovery.js +148 -47
- package/dist/private-local-service-lease.d.ts +16 -0
- package/dist/private-local-service-lease.js +105 -0
- package/dist/private-local-service-listener.d.ts +9 -2
- package/dist/private-local-service-listener.js +260 -64
- package/dist/private-local-service-log.d.ts +128 -0
- package/dist/private-local-service-log.js +321 -0
- package/dist/private-local-service-owner.d.ts +87 -1519
- package/dist/private-local-service-owner.js +1069 -71
- package/dist/private-local-service-process.js +3 -1
- package/dist/private-local-service-termination.d.ts +7 -0
- package/dist/private-local-service-termination.js +67 -0
- package/dist/private-local-service.d.ts +31 -2
- package/dist/private-local-service.js +402 -126
- package/dist/private-local-session-discovery.d.ts +52 -5
- package/dist/private-local-session-discovery.js +132 -11
- package/dist/private-local-shiitake-host.js +2 -2
- package/dist/private-local-startup-model.d.ts +51 -0
- package/dist/private-local-startup-model.js +86 -0
- package/dist/private-local-stop-all.d.ts +27 -0
- package/dist/private-local-stop-all.js +26 -0
- package/dist/private-local-ui-attachment.d.ts +19 -0
- package/dist/private-local-ui-attachment.js +71 -0
- package/dist/private-local-worker-server.d.ts +1 -0
- package/dist/private-local-worker-server.js +13 -5
- package/dist/private-mcp-form-contract.d.ts +186 -0
- package/dist/private-mcp-form-contract.js +53 -0
- package/dist/private-mcp-form-elicitation.d.ts +18 -0
- package/dist/private-mcp-form-elicitation.js +97 -0
- package/dist/private-mcp-form-session.d.ts +172 -0
- package/dist/private-mcp-form-session.js +148 -0
- package/dist/private-openai-claims.d.ts +30 -0
- package/dist/private-openai-claims.js +69 -0
- package/dist/private-oyster-mcp-auth.d.ts +13 -0
- package/dist/private-oyster-mcp-auth.js +57 -0
- package/dist/private-provider-rejection.d.ts +15 -0
- package/dist/private-provider-rejection.js +24 -0
- package/dist/private-sqlite-owner.d.ts +34 -2
- package/dist/private-sqlite-owner.js +103 -37
- package/dist/private-team-computers.d.ts +4 -4
- package/dist/private-team-computers.js +11 -2
- package/dist/seatbelt/safe-base.sbpl +79 -0
- package/dist/subscription-activation.d.ts +9 -0
- package/dist/subscription-activation.js +62 -0
- package/dist/subscription-identity.d.ts +23 -0
- package/dist/subscription-identity.js +124 -0
- package/dist/subscription-turn.d.ts +17 -0
- package/dist/subscription-turn.js +143 -0
- package/dist/team-apps.d.ts +2 -2
- package/dist/team-apps.js +7 -2
- package/native/bin/LICENSE.bubblewrap +502 -0
- package/native/bin/LICENSE.libcap +112 -0
- package/native/bin/LICENSE.sandbox-runtime +201 -0
- package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
- package/native/bin/bwrap-build-source/Dockerfile +48 -0
- package/native/bin/bwrap-build-source/README.md +96 -0
- package/native/bin/bwrap-build-source/build.mjs +52 -0
- package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
- package/native/bin/bwrap-build-source/pin.mjs +21 -0
- package/native/bin/bwrap-build-source/record.mjs +74 -0
- package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
- package/native/bin/bwrap-build-source/vendor/README.md +23 -0
- package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
- package/native/bin/bwrap-build.json +44 -0
- package/native/bin/fungi-bwrap +0 -0
- package/native/bin/fungi-bwrap.sha256 +1 -0
- package/native/bin/fungi-seccomp.bpf +0 -0
- package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
- package/package.json +44 -31
- package/dist/private-local-model-settings.d.ts +0 -5
- package/dist/private-local-model-settings.js +0 -31
- package/dist/private-local-subagents.d.ts +0 -27
- package/dist/private-local-subagents.js +0 -77
|
@@ -1,77 +1,134 @@
|
|
|
1
1
|
import { FungiMcpSelectionConflict } from "./private-fungi-mcp-selection.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { logEvent } from "./log-event.js";
|
|
3
|
+
import { readBoundedResponse } from "./private-bounded-response.js";
|
|
4
|
+
import { LocalSandboxUnavailable, sandboxUnavailableReasonSchema, } from "./private-local-containment.js";
|
|
5
|
+
export { LocalSandboxUnavailable } from "./private-local-containment.js";
|
|
6
|
+
import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
|
|
7
|
+
import { stopAllResultSchema } from "./private-local-stop-all.js";
|
|
8
|
+
import { LocalAgentSelectionAmbiguous, LocalSessionPathConflict, } from "./private-local-service-owner.js";
|
|
9
|
+
export { LocalAgentSelectionAmbiguous, LocalSessionPathConflict, } from "./private-local-service-owner.js";
|
|
4
10
|
import { EventSource } from "eventsource";
|
|
11
|
+
import { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
|
|
12
|
+
export { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
|
|
5
13
|
import { newWebSocketRpcSession } from "@fungi.computer/capnweb";
|
|
6
14
|
import { runPrivateLocalAuthRemote, } from "./private-local-auth-rpc.js";
|
|
7
15
|
import { spawn } from "node:child_process";
|
|
8
|
-
import { stat } from "node:fs/promises";
|
|
9
|
-
import { join } from "node:path";
|
|
10
16
|
import { fileURLToPath } from "node:url";
|
|
11
17
|
import { setTimeout as delay } from "node:timers/promises";
|
|
18
|
+
import { readInstalledLocalClientBuildId } from "./private-local-client-version.js";
|
|
19
|
+
export { readInstalledLocalClientBuildId } from "./private-local-client-version.js";
|
|
20
|
+
export { privateLocalServiceVersion } from "./private-local-service-discovery.js";
|
|
12
21
|
import { createShiitakeClient, parseSessionSnapshot, } from "@fungi.computer/shiitake/client";
|
|
13
|
-
import { acquireServiceLock, assertNoUntrackedServiceOwner,
|
|
22
|
+
import { acquireServiceLock, assertNoUntrackedServiceOwner, clearAbandonedServiceOwner, modelChoicesSchema, privateLocalServiceVersion, readLiveServiceStatus, readServiceRegistration, removeOwnedServiceRecord, removeUnidentifiedServiceRecord, selectedSchema, sandboxStatusSchema, sessionDiscoverySchema, archiveRefusalSchema, archiveRequestSchema, discoverRequestSchema, sessionInspectionSchema, serviceDirectory, serviceDirectoryPath, serviceOwnerIsLive, } from "./private-local-service-discovery.js";
|
|
23
|
+
import { terminatePrivateLocalService } from "./private-local-service-termination.js";
|
|
24
|
+
import { readServiceLeaseOwner } from "./private-local-service-lease.js";
|
|
14
25
|
import { privateLocalEffectiveModelSchema } from "./private-local-effective-model.js";
|
|
15
|
-
import { projectPrivateLocalSessionDiscovery } from "./private-local-session-discovery.js";
|
|
26
|
+
import { LocalStoreSchemaError, projectPrivateLocalSessionDiscovery, } from "./private-local-session-discovery.js";
|
|
27
|
+
export { LocalStoreSchemaError } from "./private-local-session-discovery.js";
|
|
16
28
|
import { capturePrivateLocalExecPath, parsePrivateLocalExecPath, } from "./private-local-exec-path.js";
|
|
17
29
|
import { z } from "zod";
|
|
30
|
+
import { serviceDiagnosticsStatusSchema } from "./private-local-service-log.js";
|
|
18
31
|
export { runPrivateLocalWorkerServer, } from "./private-local-worker-server.js";
|
|
32
|
+
/** The 409 answers a select can give; each is one case of a closed union. */
|
|
33
|
+
const selectionConflictSchema = z.discriminatedUnion("code", [
|
|
34
|
+
z.strictObject({
|
|
35
|
+
code: z.literal("sandbox_unavailable"),
|
|
36
|
+
reason: sandboxUnavailableReasonSchema,
|
|
37
|
+
}),
|
|
38
|
+
z.object({ code: z.literal("session_path_conflict") }),
|
|
39
|
+
z.object({ code: z.literal("store_in_use") }),
|
|
40
|
+
z.object({
|
|
41
|
+
code: z.literal("store_schema_incompatible"),
|
|
42
|
+
databasePath: z.string().min(1),
|
|
43
|
+
error: z.string().min(1),
|
|
44
|
+
}),
|
|
45
|
+
z.object({
|
|
46
|
+
code: z.literal("agent_selection_ambiguous"),
|
|
47
|
+
agentIds: z.array(z.string()),
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
/** Selection, creation and inspection share the listener's typed refusals. */
|
|
51
|
+
async function rejectSelectionConflict(response) {
|
|
52
|
+
const body = await response.json().catch((cause) => {
|
|
53
|
+
console.error("Local service response unavailable", cause);
|
|
54
|
+
return undefined;
|
|
55
|
+
});
|
|
56
|
+
const conflict = selectionConflictSchema.safeParse(body);
|
|
57
|
+
if (conflict.success) {
|
|
58
|
+
if (conflict.data.code === "store_schema_incompatible")
|
|
59
|
+
throw new LocalStoreSchemaError(conflict.data.databasePath);
|
|
60
|
+
if (conflict.data.code === "sandbox_unavailable")
|
|
61
|
+
throw new LocalSandboxUnavailable(conflict.data.reason);
|
|
62
|
+
if (conflict.data.code === "session_path_conflict")
|
|
63
|
+
throw new LocalSessionPathConflict();
|
|
64
|
+
if (conflict.data.code === "agent_selection_ambiguous")
|
|
65
|
+
throw new LocalAgentSelectionAmbiguous(conflict.data.agentIds);
|
|
66
|
+
throw new Error("This Agent store is held by another Fungi service. Stop the older service process, then retry; keep the Agent store files.");
|
|
67
|
+
}
|
|
68
|
+
// An Agent-selection answer without its Agents is a broken service, not
|
|
69
|
+
// an MCP connection conflict.
|
|
70
|
+
if (z.object({ code: z.literal("agent_selection_ambiguous") }).safeParse(body)
|
|
71
|
+
.success)
|
|
72
|
+
throw new Error("Local service sent an unreadable Agent selection");
|
|
73
|
+
throw new FungiMcpSelectionConflict();
|
|
74
|
+
}
|
|
75
|
+
/** The listener's body for a failure it did not expect. */
|
|
76
|
+
const serviceFailureSchema = z.object({
|
|
77
|
+
code: z.literal("internal_error"),
|
|
78
|
+
error: z.string(),
|
|
79
|
+
});
|
|
80
|
+
/** The service answered a selection with a failure status and no readable failure body. */
|
|
81
|
+
export class LocalServiceUnreadableResponse extends Error {
|
|
82
|
+
name = "LocalServiceUnreadableResponse";
|
|
83
|
+
status;
|
|
84
|
+
constructor(status, cause) {
|
|
85
|
+
super(`Local service selection failed (${String(status)}) without a readable failure body`, { cause });
|
|
86
|
+
this.status = status;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A newer client replaced the service this running client reached with one
|
|
91
|
+
* that speaks another protocol. Only restarting the window can reconnect it.
|
|
92
|
+
*/
|
|
93
|
+
export class LocalServiceUpdated extends Error {
|
|
94
|
+
name = "LocalServiceUpdated";
|
|
95
|
+
constructor() {
|
|
96
|
+
super("The Fungi service was updated. Restart this window.");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
19
99
|
/** Construction does not start a service or open a store. Call start/connect explicitly. */
|
|
20
100
|
export function createPrivateLocalServiceManager(options = {}) {
|
|
101
|
+
const root = capturePrivateLocalConfigRoot(options.home);
|
|
102
|
+
const selectedDirectory = serviceDirectoryPath(options.directory ?? root.serviceDirectory);
|
|
103
|
+
const reportCleanupFailure = options.reportCleanupFailure ??
|
|
104
|
+
((cause) => {
|
|
105
|
+
process.stderr.write(`Local service cleanup failed (${cause instanceof Error ? cause.name : "NonError"}).\n`);
|
|
106
|
+
});
|
|
21
107
|
const clientPath = options.executionPath === undefined
|
|
22
108
|
? capturePrivateLocalExecPath(process.env.PATH ?? "/usr/bin:/bin", process.cwd())
|
|
23
109
|
: parsePrivateLocalExecPath(options.executionPath);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
110
|
+
const expectedVersion = privateLocalServiceVersion();
|
|
111
|
+
/**
|
|
112
|
+
* Set once this client has reached a service. Only a starting client
|
|
113
|
+
* replaces a service speaking another protocol. Same-protocol build changes
|
|
114
|
+
* still require the installed-client check; reaching a service is not proof
|
|
115
|
+
* that this window remains the installed client.
|
|
116
|
+
*/
|
|
117
|
+
let reachedService = false;
|
|
118
|
+
function accept(service) {
|
|
119
|
+
reachedService = true;
|
|
120
|
+
return service;
|
|
33
121
|
}
|
|
34
122
|
const maximumInspectionResponseBytes = 2_097_152;
|
|
35
|
-
async function readBoundedResponse(response) {
|
|
36
|
-
const declared = response.headers.get("content-length");
|
|
37
|
-
if (declared !== null) {
|
|
38
|
-
const declaredBytes = Number(declared);
|
|
39
|
-
if (!Number.isSafeInteger(declaredBytes) ||
|
|
40
|
-
declaredBytes < 0 ||
|
|
41
|
-
declaredBytes > maximumInspectionResponseBytes)
|
|
42
|
-
throw new Error("Local Session inspection response is too large");
|
|
43
|
-
}
|
|
44
|
-
if (response.body === null)
|
|
45
|
-
throw new Error("Local Session inspection returned no body");
|
|
46
|
-
const stream = response.body.pipeThrough(new TransformStream());
|
|
47
|
-
const reader = stream.getReader();
|
|
48
|
-
const chunks = [];
|
|
49
|
-
let size = 0;
|
|
50
|
-
for (;;) {
|
|
51
|
-
const next = await reader.read();
|
|
52
|
-
if (next.done)
|
|
53
|
-
break;
|
|
54
|
-
const bytes = next.value;
|
|
55
|
-
size += bytes.byteLength;
|
|
56
|
-
if (size > maximumInspectionResponseBytes) {
|
|
57
|
-
await reader.cancel();
|
|
58
|
-
throw new Error("Local Session inspection response is too large");
|
|
59
|
-
}
|
|
60
|
-
chunks.push(bytes);
|
|
61
|
-
}
|
|
62
|
-
const joined = new Uint8Array(size);
|
|
63
|
-
let offset = 0;
|
|
64
|
-
for (const chunk of chunks) {
|
|
65
|
-
joined.set(chunk, offset);
|
|
66
|
-
offset += chunk.byteLength;
|
|
67
|
-
}
|
|
68
|
-
return new TextDecoder().decode(joined);
|
|
69
|
-
}
|
|
70
123
|
async function status(timeoutMs) {
|
|
71
|
-
const
|
|
72
|
-
|
|
124
|
+
const directory = selectedDirectory;
|
|
125
|
+
const registration = await readServiceRegistration(directory);
|
|
126
|
+
if (registration?.kind === "other-protocol" &&
|
|
127
|
+
(await serviceOwnerIsLive(directory)))
|
|
128
|
+
throw new LocalServiceUpdated();
|
|
129
|
+
if (registration?.kind !== "current")
|
|
73
130
|
return undefined;
|
|
74
|
-
const probe = await readLiveServiceStatus(record, timeoutMs);
|
|
131
|
+
const probe = await readLiveServiceStatus(registration.record, timeoutMs);
|
|
75
132
|
return probe.kind === "running" ? probe.service : undefined;
|
|
76
133
|
}
|
|
77
134
|
/**
|
|
@@ -85,38 +142,94 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
85
142
|
while (Date.now() < deadline) {
|
|
86
143
|
request.signal?.throwIfAborted();
|
|
87
144
|
const ready = await status(Math.min(10_000, waitMs));
|
|
88
|
-
if (ready)
|
|
89
|
-
|
|
145
|
+
if (ready) {
|
|
146
|
+
if (ready.version === expectedVersion)
|
|
147
|
+
return accept(ready);
|
|
148
|
+
// It answered at last, as another version: replace it under the lock.
|
|
149
|
+
const started = await startUnderLock(request);
|
|
150
|
+
if (started !== "busy")
|
|
151
|
+
return started;
|
|
152
|
+
}
|
|
90
153
|
await delay(250, undefined, { signal: request.signal });
|
|
91
154
|
}
|
|
92
155
|
throw new Error(`The Fungi service is running but has not answered for ${String(Math.round(waitMs / 1000))} seconds; the machine may be overloaded. Retry, or run fungi stop after its turns finish.`);
|
|
93
156
|
}
|
|
94
|
-
async function ownerLockReleased(directory) {
|
|
95
|
-
try {
|
|
96
|
-
await stat(join(directory, "owner.lock"));
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
catch (cause) {
|
|
100
|
-
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
101
|
-
return true;
|
|
102
|
-
throw cause;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
157
|
async function start(request = {}) {
|
|
106
158
|
const started = await startUnderLock(request);
|
|
107
159
|
return started === "busy" ? awaitBusyService(request) : started;
|
|
108
160
|
}
|
|
161
|
+
/** `fungi stop`'s work, under the startup lock that serializes it with starts. */
|
|
162
|
+
async function stopUnderLock(directory, expectedServiceId) {
|
|
163
|
+
const registration = await readServiceRegistration(directory);
|
|
164
|
+
if (registration === undefined)
|
|
165
|
+
return { kind: "not-running", recordCleaned: false };
|
|
166
|
+
if (registration.kind === "unidentified") {
|
|
167
|
+
if (await serviceOwnerIsLive(directory))
|
|
168
|
+
throw new Error(`A Fungi service this client cannot identify owns ${directory}. End that Fungi service process, then retry; saved Sessions are kept.`);
|
|
169
|
+
await removeUnidentifiedServiceRecord(directory);
|
|
170
|
+
return { kind: "not-running", recordCleaned: true };
|
|
171
|
+
}
|
|
172
|
+
const lifecycle = registration.kind === "current"
|
|
173
|
+
? registration.record
|
|
174
|
+
: registration.lifecycle;
|
|
175
|
+
if (expectedServiceId !== undefined &&
|
|
176
|
+
lifecycle.serviceId !== expectedServiceId)
|
|
177
|
+
return { kind: "replaced" };
|
|
178
|
+
const owner = await readServiceLeaseOwner(directory);
|
|
179
|
+
if (owner === undefined) {
|
|
180
|
+
await removeOwnedServiceRecord(directory, lifecycle.serviceId);
|
|
181
|
+
return { kind: "not-running", recordCleaned: true };
|
|
182
|
+
}
|
|
183
|
+
// The file points at a claim; it cannot supply signal authority itself.
|
|
184
|
+
if (owner.pid !== lifecycle.pid || owner.startedAt !== lifecycle.startedAt)
|
|
185
|
+
throw new Error(`The Fungi lease owner in ${directory} does not match service.json; retry after the owner registers itself. No process was signalled.`);
|
|
186
|
+
const outcome = await terminatePrivateLocalService(directory, owner);
|
|
187
|
+
// A killed process cannot remove its own record or lifetime lock.
|
|
188
|
+
await removeOwnedServiceRecord(directory, lifecycle.serviceId);
|
|
189
|
+
await clearAbandonedServiceOwner(directory);
|
|
190
|
+
return { kind: outcome };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Reuse the registered service when this client can, otherwise stop it so a
|
|
194
|
+
* service of this version can start; undefined means start one.
|
|
195
|
+
*/
|
|
196
|
+
async function reuseOrReplace(directory, registration) {
|
|
197
|
+
if (registration.kind === "current") {
|
|
198
|
+
const probe = await readLiveServiceStatus(registration.record);
|
|
199
|
+
// Not answering: the lifetime lease decides between busy and gone.
|
|
200
|
+
if (probe.kind !== "running")
|
|
201
|
+
return undefined;
|
|
202
|
+
if (probe.service.version === expectedVersion)
|
|
203
|
+
return probe.service;
|
|
204
|
+
// Replace only for the installed client. A stale window (or an
|
|
205
|
+
// uninstalled source run) must not stop the service; it restarts.
|
|
206
|
+
if (expectedVersion !== (await readInstalledLocalClientBuildId()))
|
|
207
|
+
throw new LocalServiceUpdated();
|
|
208
|
+
}
|
|
209
|
+
else if (registration.kind === "other-protocol" && reachedService) {
|
|
210
|
+
if (await serviceOwnerIsLive(directory))
|
|
211
|
+
throw new LocalServiceUpdated();
|
|
212
|
+
}
|
|
213
|
+
if (registration.kind === "other-protocol" &&
|
|
214
|
+
expectedVersion !== (await readInstalledLocalClientBuildId()))
|
|
215
|
+
throw new LocalServiceUpdated();
|
|
216
|
+
await stopUnderLock(directory);
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
109
219
|
async function startUnderLock(request) {
|
|
110
220
|
request.signal?.throwIfAborted();
|
|
111
|
-
const directory = await serviceDirectory(
|
|
221
|
+
const directory = await serviceDirectory(selectedDirectory);
|
|
112
222
|
const release = await acquireServiceLock(directory, "startup");
|
|
113
223
|
try {
|
|
114
|
-
const
|
|
224
|
+
const registration = await readServiceRegistration(directory);
|
|
225
|
+
const existing = registration === undefined
|
|
226
|
+
? undefined
|
|
227
|
+
: await reuseOrReplace(directory, registration);
|
|
115
228
|
request.signal?.throwIfAborted();
|
|
116
229
|
if (existing)
|
|
117
|
-
return
|
|
230
|
+
return accept(existing);
|
|
118
231
|
await assertNoUntrackedServiceOwner(directory);
|
|
119
|
-
if (serviceOwnerIsLive(directory))
|
|
232
|
+
if (await serviceOwnerIsLive(directory))
|
|
120
233
|
return "busy";
|
|
121
234
|
const bundledCli = process.env.FUNGI_BUNDLED_CLI;
|
|
122
235
|
const child = spawn(options.executable ?? process.execPath, bundledCli === undefined
|
|
@@ -124,7 +237,13 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
124
237
|
fileURLToPath(new URL("./private-local-service-process.js", import.meta.url)),
|
|
125
238
|
directory,
|
|
126
239
|
]
|
|
127
|
-
: [bundledCli, "--internal-service", directory], {
|
|
240
|
+
: [bundledCli, "--internal-service", directory], {
|
|
241
|
+
detached: true,
|
|
242
|
+
stdio: "ignore",
|
|
243
|
+
// Service startup retains its existing inherited inputs. This is
|
|
244
|
+
// not a Computer exec child: only its selected host HOME is pinned.
|
|
245
|
+
env: { ...process.env, HOME: root.home },
|
|
246
|
+
});
|
|
128
247
|
let failure;
|
|
129
248
|
let exited = false;
|
|
130
249
|
const readFailure = () => failure;
|
|
@@ -141,8 +260,11 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
141
260
|
if (childFailure !== undefined)
|
|
142
261
|
throw childFailure;
|
|
143
262
|
const ready = await status();
|
|
144
|
-
if (ready)
|
|
145
|
-
|
|
263
|
+
if (ready) {
|
|
264
|
+
if (ready.version !== expectedVersion)
|
|
265
|
+
throw new Error(`The started Local service reports version ${ready.version}, not this client's ${expectedVersion}`);
|
|
266
|
+
return accept(ready);
|
|
267
|
+
}
|
|
146
268
|
if (hasExited())
|
|
147
269
|
throw new Error("Local service exited before its handshake");
|
|
148
270
|
await delay(100, undefined, { signal: request.signal });
|
|
@@ -158,59 +280,183 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
158
280
|
await release();
|
|
159
281
|
}
|
|
160
282
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const service = await start(request);
|
|
283
|
+
/** POST one selection to `service`; every refusal is a typed failure. */
|
|
284
|
+
const postSelection = async (endpoint, input, service, signal) => {
|
|
164
285
|
const init = {
|
|
165
286
|
method: "POST",
|
|
166
287
|
headers: { "Content-Type": "application/json" },
|
|
167
288
|
body: JSON.stringify({ ...input, clientPath }),
|
|
168
289
|
redirect: "error",
|
|
169
290
|
};
|
|
170
|
-
if (
|
|
171
|
-
init.signal =
|
|
291
|
+
if (signal !== undefined)
|
|
292
|
+
init.signal = signal;
|
|
172
293
|
const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/${endpoint}`, init);
|
|
173
|
-
if (response.status === 409)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
294
|
+
if (response.status === 409)
|
|
295
|
+
await rejectSelectionConflict(response);
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
// The service names its own failure in a JSON body; one without it is
|
|
298
|
+
// a broken response, not a status to guess from.
|
|
299
|
+
const failure = serviceFailureSchema.safeParse(await response.json().catch((cause) => {
|
|
300
|
+
throw new LocalServiceUnreadableResponse(response.status, cause);
|
|
177
301
|
}));
|
|
178
|
-
if (
|
|
179
|
-
throw new
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
302
|
+
if (!failure.success)
|
|
303
|
+
throw new LocalServiceUnreadableResponse(response.status, failure.error);
|
|
304
|
+
throw new Error(`Local service selection failed (${String(response.status)}): ${failure.data.error}`);
|
|
305
|
+
}
|
|
306
|
+
return selectedSchema.parse(await response.json());
|
|
307
|
+
};
|
|
308
|
+
const routeBase = (service, selected) => `${service.baseUrl}/service/${service.serviceId}${selected.routePath}`;
|
|
309
|
+
const openConnection = async (endpoint, input, request = {}) => {
|
|
310
|
+
request.signal?.throwIfAborted();
|
|
311
|
+
const service = await start(request);
|
|
312
|
+
const selected = await postSelection(endpoint, input, service, request.signal);
|
|
313
|
+
const firstBase = routeBase(service, selected);
|
|
314
|
+
/**
|
|
315
|
+
* Where this connection's client currently talks. A restarted service
|
|
316
|
+
* comes back on a new port with a new id, so the client's requests are
|
|
317
|
+
* redirected here rather than the client rebuilt: its Session address
|
|
318
|
+
* and everything the TUI keyed on it stay the same.
|
|
319
|
+
*/
|
|
320
|
+
let route = { service, base: firstBase };
|
|
321
|
+
let closing;
|
|
322
|
+
const isClosing = () => closing !== undefined;
|
|
323
|
+
const reroute = (url) => url === firstBase || url.startsWith(`${firstBase}/`)
|
|
324
|
+
? route.base + url.slice(firstBase.length)
|
|
325
|
+
: url;
|
|
326
|
+
const transport = globalThis.fetch;
|
|
327
|
+
const releaseRoute = async (base) => {
|
|
328
|
+
try {
|
|
329
|
+
const response = await transport(`${base}/`, {
|
|
330
|
+
method: "DELETE",
|
|
331
|
+
redirect: "error",
|
|
332
|
+
signal: AbortSignal.timeout(5_000),
|
|
333
|
+
});
|
|
334
|
+
if (!response.ok && response.status !== 404)
|
|
335
|
+
throw new Error(`Local client release failed (${String(response.status)})`);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
if (options.reportCleanupFailure === undefined)
|
|
339
|
+
logEvent("service-client.release", error);
|
|
340
|
+
else
|
|
341
|
+
options.reportCleanupFailure(error);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const reroutedFetch = Object.assign((target, init) => {
|
|
345
|
+
if (target instanceof Request)
|
|
346
|
+
throw new TypeError("A Local service connection fetches by URL");
|
|
347
|
+
return transport(reroute(String(target)), init);
|
|
348
|
+
}, { preconnect: transport.preconnect });
|
|
349
|
+
const Source = options.eventSource ?? EventSource;
|
|
350
|
+
class ReroutedEventSource extends Source {
|
|
351
|
+
constructor(url) {
|
|
352
|
+
super(reroute(url));
|
|
353
|
+
// The library would retry the address it was built with, forever and
|
|
354
|
+
// silently. A dropped stream is surfaced instead, so the owner of the
|
|
355
|
+
// connection can rebind it and reopen the observation.
|
|
356
|
+
this.addEventListener("error", () => {
|
|
357
|
+
this.close();
|
|
358
|
+
});
|
|
359
|
+
}
|
|
183
360
|
}
|
|
184
|
-
if (!response.ok)
|
|
185
|
-
throw new Error(`Local service selection failed (${String(response.status)})`);
|
|
186
|
-
const selected = selectedSchema.parse(await response.json());
|
|
187
|
-
const baseUrl = `${service.baseUrl}/service/${service.serviceId}${selected.routePath}`;
|
|
188
361
|
const client = createShiitakeClient({
|
|
189
|
-
baseUrl,
|
|
190
|
-
|
|
362
|
+
baseUrl: firstBase,
|
|
363
|
+
fetch: reroutedFetch,
|
|
364
|
+
eventSource: ReroutedEventSource,
|
|
191
365
|
});
|
|
192
366
|
return {
|
|
193
|
-
service
|
|
367
|
+
get service() {
|
|
368
|
+
return route.service;
|
|
369
|
+
},
|
|
194
370
|
agentId: selected.agentId,
|
|
195
371
|
databasePath: selected.databasePath,
|
|
196
372
|
sessionId: selected.sessionId,
|
|
197
373
|
client,
|
|
198
374
|
session: client.session(selected.sessionId),
|
|
375
|
+
sandboxStatus: async (request = {}) => {
|
|
376
|
+
request.signal?.throwIfAborted();
|
|
377
|
+
const init = { redirect: "error" };
|
|
378
|
+
if (request.signal !== undefined)
|
|
379
|
+
init.signal = request.signal;
|
|
380
|
+
const response = await transport(`${route.base}/sandbox`, init);
|
|
381
|
+
if (!response.ok)
|
|
382
|
+
throw new Error(`Local sandbox status read failed (${String(response.status)})`);
|
|
383
|
+
return sandboxStatusSchema.parse(await response.json());
|
|
384
|
+
},
|
|
385
|
+
stopAll: async () => {
|
|
386
|
+
const response = await fetch(`${route.service.baseUrl}/service/${route.service.serviceId}/stop-all`, {
|
|
387
|
+
method: "POST",
|
|
388
|
+
headers: { "Content-Type": "application/json" },
|
|
389
|
+
body: JSON.stringify({
|
|
390
|
+
invocationRoot: input.invocationRoot,
|
|
391
|
+
agentId: selected.agentId,
|
|
392
|
+
}),
|
|
393
|
+
redirect: "error",
|
|
394
|
+
});
|
|
395
|
+
if (!response.ok)
|
|
396
|
+
throw new Error(`Stop all failed (${String(response.status)})`);
|
|
397
|
+
return stopAllResultSchema.parse(await response.json());
|
|
398
|
+
},
|
|
199
399
|
effectiveModel: async (request = {}) => {
|
|
200
400
|
request.signal?.throwIfAborted();
|
|
201
401
|
const init = { redirect: "error" };
|
|
202
402
|
if (request.signal !== undefined)
|
|
203
403
|
init.signal = request.signal;
|
|
204
|
-
const response = await
|
|
404
|
+
const response = await transport(`${route.base}/sessions/${encodeURIComponent(selected.sessionId)}/effective-model`, init);
|
|
205
405
|
if (!response.ok)
|
|
206
406
|
throw new Error(`Local effective model read failed (${String(response.status)})`);
|
|
207
407
|
return privateLocalEffectiveModelSchema.parse(await response.json());
|
|
208
408
|
},
|
|
209
|
-
|
|
409
|
+
rebind: async (rebindRequest = {}) => {
|
|
410
|
+
if (isClosing())
|
|
411
|
+
throw new Error("Local service connection is closed");
|
|
412
|
+
rebindRequest.signal?.throwIfAborted();
|
|
413
|
+
// A healthy probe does not guarantee that selection will answer.
|
|
414
|
+
// Bound each attempt independently of the pane's lifetime.
|
|
415
|
+
const deadline = AbortSignal.timeout(5_000);
|
|
416
|
+
const signal = rebindRequest.signal === undefined
|
|
417
|
+
? deadline
|
|
418
|
+
: AbortSignal.any([rebindRequest.signal, deadline]);
|
|
419
|
+
// Read service.json again, and never start a service: whoever
|
|
420
|
+
// stopped it decides when it returns.
|
|
421
|
+
const ready = await status();
|
|
422
|
+
if (ready === undefined)
|
|
423
|
+
return { kind: "unavailable" };
|
|
424
|
+
accept(ready);
|
|
425
|
+
const again = await postSelection("select", {
|
|
426
|
+
invocationRoot: input.invocationRoot,
|
|
427
|
+
databasePath: selected.databasePath,
|
|
428
|
+
sessionId: selected.sessionId,
|
|
429
|
+
fungiMcp: input.fungiMcp,
|
|
430
|
+
uiAttachmentLifetime: input.uiAttachmentLifetime,
|
|
431
|
+
}, ready, signal);
|
|
432
|
+
if (again.agentId !== selected.agentId ||
|
|
433
|
+
again.sessionId !== selected.sessionId)
|
|
434
|
+
throw new Error("The restarted Local service selected a different Session");
|
|
435
|
+
const nextBase = routeBase(ready, again);
|
|
436
|
+
if (isClosing()) {
|
|
437
|
+
await releaseRoute(nextBase);
|
|
438
|
+
throw new Error("Local service connection is closed");
|
|
439
|
+
}
|
|
440
|
+
const previous = route;
|
|
441
|
+
route = { service: ready, base: nextBase };
|
|
442
|
+
if (previous.service.serviceId === ready.serviceId)
|
|
443
|
+
await releaseRoute(previous.base);
|
|
444
|
+
return { kind: "rebound", service: ready };
|
|
445
|
+
},
|
|
446
|
+
close: () => {
|
|
447
|
+
closing ??= (async () => {
|
|
448
|
+
const base = route.base;
|
|
449
|
+
// Stop local observations first, including when the service is gone.
|
|
450
|
+
await client.close();
|
|
451
|
+
await releaseRoute(base);
|
|
452
|
+
})();
|
|
453
|
+
return closing;
|
|
454
|
+
},
|
|
210
455
|
};
|
|
211
456
|
};
|
|
212
457
|
const connect = (selection, request) => openConnection("select", selection, request);
|
|
213
458
|
const inspectResponseSchema = z.strictObject({
|
|
459
|
+
diagnostics: serviceDiagnosticsStatusSchema,
|
|
214
460
|
selected: selectedSchema,
|
|
215
461
|
snapshot: z.unknown(),
|
|
216
462
|
});
|
|
@@ -225,7 +471,10 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
225
471
|
const init = {
|
|
226
472
|
method: "POST",
|
|
227
473
|
headers: { "Content-Type": "application/json" },
|
|
228
|
-
body: JSON.stringify({
|
|
474
|
+
body: JSON.stringify(discoverRequestSchema.parse({
|
|
475
|
+
invocationRoot,
|
|
476
|
+
archived: request.archived,
|
|
477
|
+
})),
|
|
229
478
|
redirect: "error",
|
|
230
479
|
};
|
|
231
480
|
if (request.signal !== undefined)
|
|
@@ -235,6 +484,32 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
235
484
|
throw new Error(`Local session discovery failed (${String(response.status)})`);
|
|
236
485
|
return projectPrivateLocalSessionDiscovery(sessionDiscoverySchema.parse(await response.json()), connect);
|
|
237
486
|
},
|
|
487
|
+
async archive(archival, request = {}) {
|
|
488
|
+
request.signal?.throwIfAborted();
|
|
489
|
+
const service = await start(request);
|
|
490
|
+
const init = {
|
|
491
|
+
method: "POST",
|
|
492
|
+
headers: { "Content-Type": "application/json" },
|
|
493
|
+
body: JSON.stringify(archiveRequestSchema.parse(archival)),
|
|
494
|
+
redirect: "error",
|
|
495
|
+
};
|
|
496
|
+
if (request.signal !== undefined)
|
|
497
|
+
init.signal = request.signal;
|
|
498
|
+
const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/archive`, init);
|
|
499
|
+
if (response.status === 409) {
|
|
500
|
+
const refusal = archiveRefusalSchema.safeParse(await response.json().catch((cause) => {
|
|
501
|
+
throw new LocalServiceUnreadableResponse(response.status, cause);
|
|
502
|
+
}));
|
|
503
|
+
if (refusal.success)
|
|
504
|
+
throw new AgentArchiveRefused(refusal.data.reason, refusal.data.error);
|
|
505
|
+
}
|
|
506
|
+
if (!response.ok) {
|
|
507
|
+
const failure = serviceFailureSchema.safeParse(await response.json().catch((cause) => {
|
|
508
|
+
throw new LocalServiceUnreadableResponse(response.status, cause);
|
|
509
|
+
}));
|
|
510
|
+
throw new Error(`Local Agent archive failed (${String(response.status)})${failure.success ? `: ${failure.data.error}` : ""}`);
|
|
511
|
+
}
|
|
512
|
+
},
|
|
238
513
|
async inspect(selection, request = {}) {
|
|
239
514
|
request.signal?.throwIfAborted();
|
|
240
515
|
const service = await start(request);
|
|
@@ -251,15 +526,18 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
251
526
|
if (request.signal !== undefined)
|
|
252
527
|
init.signal = request.signal;
|
|
253
528
|
const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/inspect`, init);
|
|
529
|
+
if (response.status === 409)
|
|
530
|
+
await rejectSelectionConflict(response);
|
|
254
531
|
if (!response.ok)
|
|
255
532
|
throw new Error(`Local Session inspection failed (${String(response.status)})`);
|
|
256
|
-
const decoded = inspectResponseSchema.parse(JSON.parse(await readBoundedResponse(response)));
|
|
533
|
+
const decoded = inspectResponseSchema.parse(JSON.parse(await readBoundedResponse(response, maximumInspectionResponseBytes, "Local Session inspection")));
|
|
257
534
|
const baseUrl = `${service.baseUrl}/service/${service.serviceId}${decoded.selected.routePath}`;
|
|
258
535
|
const client = createShiitakeClient({
|
|
259
536
|
baseUrl,
|
|
260
537
|
eventSource: options.eventSource ?? EventSource,
|
|
261
538
|
});
|
|
262
539
|
return {
|
|
540
|
+
diagnostics: decoded.diagnostics,
|
|
263
541
|
session: client.session(decoded.selected.sessionId),
|
|
264
542
|
snapshot: parseSessionSnapshot(JSON.stringify(decoded.snapshot)),
|
|
265
543
|
close: () => client.close(),
|
|
@@ -274,7 +552,7 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
274
552
|
const socket = new WebSocket(url);
|
|
275
553
|
const remote = newWebSocketRpcSession(socket);
|
|
276
554
|
try {
|
|
277
|
-
return await runPrivateLocalAuthRemote(remote, request, interaction);
|
|
555
|
+
return await runPrivateLocalAuthRemote(remote, request, interaction, reportCleanupFailure);
|
|
278
556
|
}
|
|
279
557
|
finally {
|
|
280
558
|
socket.close();
|
|
@@ -291,36 +569,34 @@ export function createPrivateLocalServiceManager(options = {}) {
|
|
|
291
569
|
throw new Error(`Local service model catalogue failed (${String(response.status)})`);
|
|
292
570
|
return modelChoicesSchema.parse(await response.json());
|
|
293
571
|
},
|
|
572
|
+
async reload(invocationRoot) {
|
|
573
|
+
const service = await status();
|
|
574
|
+
if (service === undefined)
|
|
575
|
+
throw new Error("Local service is not running");
|
|
576
|
+
const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/reload`, {
|
|
577
|
+
method: "POST",
|
|
578
|
+
headers: { "Content-Type": "application/json" },
|
|
579
|
+
body: JSON.stringify({ invocationRoot, clientPath }),
|
|
580
|
+
redirect: "error",
|
|
581
|
+
signal: AbortSignal.timeout(30_000),
|
|
582
|
+
});
|
|
583
|
+
const result = z
|
|
584
|
+
.union([
|
|
585
|
+
z.strictObject({ message: z.string() }),
|
|
586
|
+
z.strictObject({ error: z.string() }),
|
|
587
|
+
])
|
|
588
|
+
.parse(await response.json());
|
|
589
|
+
if ("error" in result)
|
|
590
|
+
throw new Error(result.error);
|
|
591
|
+
if (!response.ok)
|
|
592
|
+
throw new Error(`Configuration reload failed (${String(response.status)})`);
|
|
593
|
+
return result.message;
|
|
594
|
+
},
|
|
294
595
|
async stop(expectedServiceId) {
|
|
295
|
-
const directory = await serviceDirectory(
|
|
596
|
+
const directory = await serviceDirectory(selectedDirectory);
|
|
296
597
|
const release = await acquireServiceLock(directory, "startup");
|
|
297
598
|
try {
|
|
298
|
-
|
|
299
|
-
if (service === undefined)
|
|
300
|
-
return { kind: "not-running", recordCleaned: false };
|
|
301
|
-
if (expectedServiceId !== undefined &&
|
|
302
|
-
service.serviceId !== expectedServiceId)
|
|
303
|
-
return { kind: "replaced" };
|
|
304
|
-
const probe = await readLiveServiceStatus(service);
|
|
305
|
-
if (probe.kind === "not-running" && !serviceOwnerIsLive(directory)) {
|
|
306
|
-
await removeOwnedServiceRecord(directory, service.serviceId);
|
|
307
|
-
return { kind: "not-running", recordCleaned: true };
|
|
308
|
-
}
|
|
309
|
-
if (probe.kind !== "running")
|
|
310
|
-
throw new Error("Local service is not answering; retry after its active turns finish.");
|
|
311
|
-
const response = await fetch(`${service.baseUrl}/service/${service.serviceId}/stop`, {
|
|
312
|
-
method: "POST",
|
|
313
|
-
redirect: "error",
|
|
314
|
-
});
|
|
315
|
-
if (!response.ok)
|
|
316
|
-
throw new Error("Local service refused shutdown");
|
|
317
|
-
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
318
|
-
if (!(await handshake(service)) &&
|
|
319
|
-
(await ownerLockReleased(directory)))
|
|
320
|
-
return { kind: "stopped" };
|
|
321
|
-
await delay(100);
|
|
322
|
-
}
|
|
323
|
-
throw new Error("Local service shutdown has not released its owner lock");
|
|
599
|
+
return await stopUnderLock(directory, expectedServiceId);
|
|
324
600
|
}
|
|
325
601
|
finally {
|
|
326
602
|
await release();
|