@fungi.computer/truffle 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +294 -2
- package/computerd-loopback.cjs +16 -0
- package/dist/auth-cli.d.ts +36 -0
- package/dist/auth-cli.js +351 -0
- package/dist/fungi-account-command.d.ts +18 -0
- package/dist/fungi-account-command.js +92 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +27488 -0
- package/dist/internal/sandbox-metafile.json +6726 -0
- package/dist/linear-auth.d.ts +4 -0
- package/dist/linear-auth.js +72 -0
- package/dist/local-extensions.d.ts +97 -0
- package/dist/local-extensions.js +484 -0
- package/dist/local-login.d.ts +55 -0
- package/dist/local-login.js +167 -0
- package/dist/local-models.d.ts +14 -0
- package/dist/local-models.js +77 -0
- package/dist/local-skill-source.d.ts +6 -0
- package/dist/local-skill-source.js +357 -0
- package/dist/log-event.d.ts +2 -0
- package/dist/log-event.js +9 -0
- package/dist/private-bun-shiitake-host.d.ts +11 -0
- package/dist/private-bun-shiitake-host.js +66 -0
- package/dist/private-file-credential-store.d.ts +33 -0
- package/dist/private-file-credential-store.js +283 -0
- package/dist/private-files-patch.d.ts +3 -0
- package/dist/private-files-patch.js +86 -0
- package/dist/private-fungi-account.d.ts +235 -0
- package/dist/private-fungi-account.js +1149 -0
- package/dist/private-fungi-auth-error.d.ts +5 -0
- package/dist/private-fungi-auth-error.js +7 -0
- package/dist/private-fungi-mcp-connection.d.ts +22 -0
- package/dist/private-fungi-mcp-connection.js +143 -0
- package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
- package/dist/private-fungi-mcp-endpoint.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +14 -0
- package/dist/private-fungi-mcp-selection.js +36 -0
- package/dist/private-fungi-model-api.d.ts +6 -0
- package/dist/private-fungi-model-api.js +125 -0
- package/dist/private-fungi-model-catalog.d.ts +6 -0
- package/dist/private-fungi-model-catalog.js +78 -0
- package/dist/private-fungi-model-provider.d.ts +6 -0
- package/dist/private-fungi-model-provider.js +46 -0
- package/dist/private-fungi-pending-logouts.d.ts +34 -0
- package/dist/private-fungi-pending-logouts.js +136 -0
- package/dist/private-invocation-computer.d.ts +37 -0
- package/dist/private-invocation-computer.js +459 -0
- package/dist/private-invocation-files.d.ts +15 -0
- package/dist/private-invocation-files.js +449 -0
- package/dist/private-local-acp-stdio.d.ts +17 -0
- package/dist/private-local-acp-stdio.js +142 -0
- package/dist/private-local-auth-rpc.d.ts +69 -0
- package/dist/private-local-auth-rpc.js +273 -0
- package/dist/private-local-computer-attach.d.ts +49 -0
- package/dist/private-local-computer-attach.js +216 -0
- package/dist/private-local-computer-backend.d.ts +282 -0
- package/dist/private-local-computer-backend.js +402 -0
- package/dist/private-local-computer-processes.d.ts +199 -0
- package/dist/private-local-computer-processes.js +589 -0
- package/dist/private-local-default-model.d.ts +11 -0
- package/dist/private-local-default-model.js +45 -0
- package/dist/private-local-delegation.d.ts +29 -0
- package/dist/private-local-delegation.js +78 -0
- package/dist/private-local-effective-model.d.ts +19 -0
- package/dist/private-local-effective-model.js +14 -0
- package/dist/private-local-exec-launch.d.ts +41 -0
- package/dist/private-local-exec-launch.js +51 -0
- package/dist/private-local-exec-path.d.ts +6 -0
- package/dist/private-local-exec-path.js +40 -0
- package/dist/private-local-git-exclude.d.ts +2 -0
- package/dist/private-local-git-exclude.js +33 -0
- package/dist/private-local-http-dispatch.d.ts +5 -0
- package/dist/private-local-http-dispatch.js +14 -0
- package/dist/private-local-mail.d.ts +11 -0
- package/dist/private-local-mail.js +69 -0
- package/dist/private-local-model-settings.d.ts +5 -0
- package/dist/private-local-model-settings.js +31 -0
- package/dist/private-local-runtime-host.d.ts +141 -0
- package/dist/private-local-runtime-host.js +988 -0
- package/dist/private-local-service-contract.d.ts +90 -0
- package/dist/private-local-service-contract.js +1 -0
- package/dist/private-local-service-discovery.d.ts +101 -0
- package/dist/private-local-service-discovery.js +221 -0
- package/dist/private-local-service-ids.d.ts +3 -0
- package/dist/private-local-service-ids.js +8 -0
- package/dist/private-local-service-listener.d.ts +15 -0
- package/dist/private-local-service-listener.js +211 -0
- package/dist/private-local-service-owner.d.ts +1567 -0
- package/dist/private-local-service-owner.js +371 -0
- package/dist/private-local-service-process.d.ts +1 -0
- package/dist/private-local-service-process.js +6 -0
- package/dist/private-local-service.d.ts +17 -0
- package/dist/private-local-service.js +330 -0
- package/dist/private-local-session-discovery.d.ts +19 -0
- package/dist/private-local-session-discovery.js +157 -0
- package/dist/private-local-shiitake-host.d.ts +44 -0
- package/dist/private-local-shiitake-host.js +143 -0
- package/dist/private-local-subagents.d.ts +27 -0
- package/dist/private-local-subagents.js +77 -0
- package/dist/private-local-worker-server.d.ts +15 -0
- package/dist/private-local-worker-server.js +136 -0
- package/dist/private-sqlite-owner.d.ts +7 -0
- package/dist/private-sqlite-owner.js +103 -0
- package/dist/private-team-computers.d.ts +38 -0
- package/dist/private-team-computers.js +101 -0
- package/dist/private-workerd-sandbox.d.ts +3 -0
- package/dist/private-workerd-sandbox.js +347 -0
- package/dist/team-apps.d.ts +24 -0
- package/dist/team-apps.js +123 -0
- package/dist/tracker-git-credential.d.ts +15 -0
- package/dist/tracker-git-credential.js +87 -0
- package/native/bin/build.json +4037 -0
- package/native/bin/fungi-apply-patch +0 -0
- package/package.json +148 -3
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { logEvent } from "./log-event.js";
|
|
2
|
+
import { readFungiMcpSelection, sameFungiMcpConnection, FungiMcpSelectionConflict, } from "./private-fungi-mcp-selection.js";
|
|
3
|
+
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
4
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
5
|
+
import writeFileAtomic from "write-file-atomic";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import * as Schema from "effect/Schema";
|
|
8
|
+
import { selectionSchema } from "./private-local-service-discovery.js";
|
|
9
|
+
import { createShiitakeHttpRoutes } from "@fungi.computer/shiitake/http";
|
|
10
|
+
import { listPrivateLocalAgentStores, readPrivateLocalSessionSummaries, } from "./private-local-session-discovery.js";
|
|
11
|
+
import { createPrivateLocalSessionRequestId, openPrivateLocalRuntimeHost, resolvePrivateLocalCreateStore, } from "./private-local-runtime-host.js";
|
|
12
|
+
export class LocalSessionPathConflict extends Error {
|
|
13
|
+
name = "LocalSessionPathConflict";
|
|
14
|
+
constructor() {
|
|
15
|
+
super("PATH differs for this live session; close its clients and restart the Local service before reconnecting from another toolchain");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Reopen inputs only. Credentials remain with Local's account custodian.
|
|
19
|
+
const recoveryOwnerSchema = selectionSchema
|
|
20
|
+
.omit({ clientPath: true, sessionId: true })
|
|
21
|
+
.extend({ databasePath: z.string().min(1) });
|
|
22
|
+
const decodeRecoveryFile = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Array(Schema.Unknown)));
|
|
23
|
+
/** One service owns each store, independently of how many clients select it. */
|
|
24
|
+
export function createLocalServiceOwners(openRuntime = openPrivateLocalRuntimeHost, recoveryDirectory) {
|
|
25
|
+
const recoveryPath = recoveryDirectory === undefined
|
|
26
|
+
? undefined
|
|
27
|
+
: resolve(recoveryDirectory, "owners.json");
|
|
28
|
+
const recovery = new Map();
|
|
29
|
+
let saving = Promise.resolve();
|
|
30
|
+
const emptySessions = new Map();
|
|
31
|
+
const owners = new Map();
|
|
32
|
+
const opening = new Set();
|
|
33
|
+
const pendingOpenings = new Set();
|
|
34
|
+
let stopping = false;
|
|
35
|
+
let closing;
|
|
36
|
+
function saveRecovery() {
|
|
37
|
+
if (recoveryPath === undefined)
|
|
38
|
+
return Promise.resolve();
|
|
39
|
+
const path = recoveryPath;
|
|
40
|
+
const next = saving.then(() => writeFileAtomic(path, JSON.stringify([...recovery.values()]), {
|
|
41
|
+
mode: 0o600,
|
|
42
|
+
}));
|
|
43
|
+
// A recovery hint write must not hide a live owner or poison later saves.
|
|
44
|
+
saving = next.catch((cause) => {
|
|
45
|
+
logEvent("service-owner.save-recovery", cause);
|
|
46
|
+
});
|
|
47
|
+
return saving;
|
|
48
|
+
}
|
|
49
|
+
function trackOpening(pending, invocationRoot, key) {
|
|
50
|
+
const opened = pending;
|
|
51
|
+
// Runtime ownership survives a failed recovery-reference write. Shutdown
|
|
52
|
+
// must still drain it before the service can release its owner lock.
|
|
53
|
+
opening.add(opened);
|
|
54
|
+
void opened.catch((cause) => {
|
|
55
|
+
opening.delete(opened);
|
|
56
|
+
logEvent("service-owner.open-runtime", cause);
|
|
57
|
+
});
|
|
58
|
+
pending = pending.then(async (owner) => {
|
|
59
|
+
if (recoveryPath !== undefined) {
|
|
60
|
+
const reference = {
|
|
61
|
+
invocationRoot,
|
|
62
|
+
databasePath: owner.runtime.databasePath,
|
|
63
|
+
};
|
|
64
|
+
if (owner.fungiMcp !== undefined)
|
|
65
|
+
reference.fungiMcp = { endpoint: owner.fungiMcp.endpoint.href };
|
|
66
|
+
recovery.set(owner.runtime.databasePath, reference);
|
|
67
|
+
await saveRecovery();
|
|
68
|
+
}
|
|
69
|
+
return owner;
|
|
70
|
+
});
|
|
71
|
+
owners.set(key, pending);
|
|
72
|
+
pendingOpenings.add(pending);
|
|
73
|
+
void pending.then((owner) => {
|
|
74
|
+
owners.set(`store:${owner.runtime.databasePath}`, pending);
|
|
75
|
+
const projectKey = `project:${invocationRoot}`;
|
|
76
|
+
if (!owners.has(projectKey))
|
|
77
|
+
owners.set(projectKey, pending);
|
|
78
|
+
pendingOpenings.delete(pending);
|
|
79
|
+
}, () => {
|
|
80
|
+
if (owners.get(key) === pending)
|
|
81
|
+
owners.delete(key);
|
|
82
|
+
pendingOpenings.delete(pending);
|
|
83
|
+
});
|
|
84
|
+
return pending;
|
|
85
|
+
}
|
|
86
|
+
function ensureRunning() {
|
|
87
|
+
if (stopping)
|
|
88
|
+
throw new Error("Local service is stopping");
|
|
89
|
+
}
|
|
90
|
+
function selected(owner, sessionId) {
|
|
91
|
+
return {
|
|
92
|
+
agentId: owner.agentId,
|
|
93
|
+
databasePath: owner.databasePath,
|
|
94
|
+
sessionId,
|
|
95
|
+
session: owner.getSession(sessionId),
|
|
96
|
+
effectiveModel: owner.effectiveModel,
|
|
97
|
+
routes: createShiitakeHttpRoutes(owner.getSession, owner.listSessions, owner.createSession, owner.deleteSession, () => owner.extensions),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
async function createSession(creation) {
|
|
101
|
+
return select(creation, creation.requestId);
|
|
102
|
+
}
|
|
103
|
+
function belongsToProject(invocationRoot, databasePath) {
|
|
104
|
+
const within = relative(invocationRoot, databasePath);
|
|
105
|
+
return !isAbsolute(within) && within !== ".." && !within.startsWith("../");
|
|
106
|
+
}
|
|
107
|
+
async function selectedDatabasePath(invocationRoot, requested) {
|
|
108
|
+
const databasePath = requested === undefined
|
|
109
|
+
? undefined
|
|
110
|
+
: await realpath(resolve(invocationRoot, requested));
|
|
111
|
+
if (databasePath !== undefined || owners.has(`project:${invocationRoot}`))
|
|
112
|
+
return databasePath;
|
|
113
|
+
const stores = await listPrivateLocalAgentStores(invocationRoot);
|
|
114
|
+
ensureRunning();
|
|
115
|
+
if (stores.length > 1)
|
|
116
|
+
throw new Error("Multiple Agent stores in this folder; select a saved conversation with fungi resume or specify --database");
|
|
117
|
+
return stores[0];
|
|
118
|
+
}
|
|
119
|
+
async function ownerFor(selection) {
|
|
120
|
+
ensureRunning();
|
|
121
|
+
const invocationRoot = await realpath(selection.invocationRoot);
|
|
122
|
+
const databasePath = await selectedDatabasePath(invocationRoot, selection.databasePath);
|
|
123
|
+
ensureRunning();
|
|
124
|
+
const fungiMcp = selection.fungiMcp === undefined
|
|
125
|
+
? undefined
|
|
126
|
+
: readFungiMcpSelection(selection.fungiMcp);
|
|
127
|
+
ensureRunning();
|
|
128
|
+
// A new project selection shares its in-flight owner; explicit resume keys the store.
|
|
129
|
+
const key = databasePath === undefined
|
|
130
|
+
? `project:${invocationRoot}`
|
|
131
|
+
: `store:${databasePath}`;
|
|
132
|
+
let pending = owners.get(key);
|
|
133
|
+
if (pending === undefined && pendingOpenings.size > 0) {
|
|
134
|
+
// A fresh project does not reveal its canonical store path until opening.
|
|
135
|
+
// Let current openings install both project and store aliases, then recheck.
|
|
136
|
+
await Promise.allSettled(pendingOpenings);
|
|
137
|
+
ensureRunning();
|
|
138
|
+
pending = owners.get(key);
|
|
139
|
+
}
|
|
140
|
+
if (pending === undefined) {
|
|
141
|
+
const projectControl = databasePath === undefined ||
|
|
142
|
+
belongsToProject(invocationRoot, databasePath);
|
|
143
|
+
let options = {
|
|
144
|
+
invocationRoot,
|
|
145
|
+
databasePath,
|
|
146
|
+
agentControl: projectControl ? directory(invocationRoot) : undefined,
|
|
147
|
+
mail: projectControl ? mailRouting(invocationRoot) : undefined,
|
|
148
|
+
};
|
|
149
|
+
if (fungiMcp !== undefined)
|
|
150
|
+
options = { ...options, fungiMcp };
|
|
151
|
+
pending = trackOpening(openRuntime(options).then((runtime) => ({ runtime, fungiMcp })), invocationRoot, key);
|
|
152
|
+
}
|
|
153
|
+
const owner = await pending;
|
|
154
|
+
ensureRunning();
|
|
155
|
+
if (fungiMcp !== undefined &&
|
|
156
|
+
!sameFungiMcpConnection(owner.fungiMcp, fungiMcp))
|
|
157
|
+
throw new FungiMcpSelectionConflict();
|
|
158
|
+
return owner.runtime;
|
|
159
|
+
}
|
|
160
|
+
async function select(selection, createRequestId) {
|
|
161
|
+
const owner = await ownerFor(selection);
|
|
162
|
+
let sessionId = selection.sessionId;
|
|
163
|
+
if (createRequestId !== undefined) {
|
|
164
|
+
sessionId = await owner.createSession(createRequestId);
|
|
165
|
+
ensureRunning();
|
|
166
|
+
}
|
|
167
|
+
else if (sessionId === undefined) {
|
|
168
|
+
const summaries = await readPrivateLocalSessionSummaries(owner.databasePath);
|
|
169
|
+
sessionId = summaries[0]?.sessionId;
|
|
170
|
+
if (sessionId === undefined) {
|
|
171
|
+
let empty = emptySessions.get(owner.databasePath);
|
|
172
|
+
if (empty === undefined) {
|
|
173
|
+
empty = owner.createSession(createPrivateLocalSessionRequestId());
|
|
174
|
+
emptySessions.set(owner.databasePath, empty);
|
|
175
|
+
}
|
|
176
|
+
sessionId = await empty;
|
|
177
|
+
}
|
|
178
|
+
ensureRunning();
|
|
179
|
+
}
|
|
180
|
+
if (selection.clientPath !== undefined &&
|
|
181
|
+
!owner.bindSessionPath(sessionId, selection.clientPath))
|
|
182
|
+
throw new LocalSessionPathConflict();
|
|
183
|
+
return selected(owner, sessionId);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Project mail routing for every Agent this service owns in one project.
|
|
187
|
+
* Delivery opens an idle addressed Agent, so its admitted mail wakes it; the
|
|
188
|
+
* recipient's own ingress decides what it accepts.
|
|
189
|
+
*/
|
|
190
|
+
function mailRouting(invocationRoot) {
|
|
191
|
+
const control = directory(invocationRoot);
|
|
192
|
+
return {
|
|
193
|
+
deliver: async (mail) => (await control.owner(mail.to.agentId, new AbortController().signal)).acceptMail(mail),
|
|
194
|
+
readSettlement: async (agentId, sessionId, commandId) => (await control.owner(agentId, new AbortController().signal)).readSettlement(sessionId, commandId),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function directory(invocationRoot) {
|
|
198
|
+
async function targets(signal) {
|
|
199
|
+
ensureRunning();
|
|
200
|
+
signal.throwIfAborted();
|
|
201
|
+
const paths = await listPrivateLocalAgentStores(invocationRoot);
|
|
202
|
+
signal.throwIfAborted();
|
|
203
|
+
const result = new Map();
|
|
204
|
+
for (const databasePath of paths) {
|
|
205
|
+
// Discovery can follow symlinks for human resume; model control cannot
|
|
206
|
+
// cross the canonical project boundary through one of those links.
|
|
207
|
+
if (!belongsToProject(invocationRoot, databasePath))
|
|
208
|
+
continue;
|
|
209
|
+
const agentId = basename(dirname(databasePath));
|
|
210
|
+
if (result.has(agentId))
|
|
211
|
+
throw new Error("Local Agent identity is ambiguous");
|
|
212
|
+
result.set(agentId, databasePath);
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
async function admittedOwner(agentId, signal) {
|
|
217
|
+
const databasePath = (await targets(signal)).get(agentId);
|
|
218
|
+
if (databasePath === undefined)
|
|
219
|
+
throw new Error("Agent is outside this Local project");
|
|
220
|
+
const owner = await ownerFor({ invocationRoot, databasePath });
|
|
221
|
+
if ((await targets(signal)).get(agentId) !== databasePath)
|
|
222
|
+
throw new Error("Agent is outside this Local project");
|
|
223
|
+
signal.throwIfAborted();
|
|
224
|
+
ensureRunning();
|
|
225
|
+
return owner;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
owner: admittedOwner,
|
|
229
|
+
list: async (signal) => [...(await targets(signal)).keys()].map((agentId) => ({
|
|
230
|
+
agentId,
|
|
231
|
+
name: agentId,
|
|
232
|
+
})),
|
|
233
|
+
create: async ({ requestId, model }, signal) => {
|
|
234
|
+
ensureRunning();
|
|
235
|
+
signal.throwIfAborted();
|
|
236
|
+
const key = `create:${JSON.stringify([invocationRoot, requestId])}`;
|
|
237
|
+
const createStore = await resolvePrivateLocalCreateStore(invocationRoot, requestId);
|
|
238
|
+
signal.throwIfAborted();
|
|
239
|
+
ensureRunning();
|
|
240
|
+
const databasePath = createStore.databasePath;
|
|
241
|
+
let pending = owners.get(key);
|
|
242
|
+
if (pending === undefined) {
|
|
243
|
+
pending = owners.get(`store:${databasePath}`);
|
|
244
|
+
if (pending === undefined) {
|
|
245
|
+
let options = {
|
|
246
|
+
invocationRoot,
|
|
247
|
+
createRequestId: requestId,
|
|
248
|
+
agentControl: directory(invocationRoot),
|
|
249
|
+
mail: mailRouting(invocationRoot),
|
|
250
|
+
};
|
|
251
|
+
if (model !== undefined)
|
|
252
|
+
options = {
|
|
253
|
+
...options,
|
|
254
|
+
providerId: model.providerId,
|
|
255
|
+
modelId: model.modelId,
|
|
256
|
+
reasoning: model.reasoning,
|
|
257
|
+
};
|
|
258
|
+
pending = trackOpening(openRuntime(options).then((runtime) => ({
|
|
259
|
+
runtime,
|
|
260
|
+
fungiMcp: undefined,
|
|
261
|
+
})), invocationRoot, key);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const owner = await pending;
|
|
265
|
+
signal.throwIfAborted();
|
|
266
|
+
ensureRunning();
|
|
267
|
+
if (owner.runtime.databasePath !== databasePath)
|
|
268
|
+
throw new Error("Local Agent create identity does not match its store");
|
|
269
|
+
owners.set(key, pending);
|
|
270
|
+
await owner.runtime.assertModelReady("main", signal);
|
|
271
|
+
const agentId = createStore.agentId;
|
|
272
|
+
return Object.freeze({ agentId, name: agentId });
|
|
273
|
+
},
|
|
274
|
+
setModel: async ({ agentId, model }, signal) => {
|
|
275
|
+
const owner = await admittedOwner(agentId, signal);
|
|
276
|
+
await owner.setModel(model, signal);
|
|
277
|
+
},
|
|
278
|
+
checkModel: async ({ agentId, sessionId }, signal) => {
|
|
279
|
+
const owner = await admittedOwner(agentId, signal);
|
|
280
|
+
await owner.assertModelReady(sessionId, signal);
|
|
281
|
+
},
|
|
282
|
+
open: async (agentId, signal) => {
|
|
283
|
+
const owner = await admittedOwner(agentId, signal);
|
|
284
|
+
// The service retains runtime ownership; an operation leases access only.
|
|
285
|
+
const release = () => Promise.resolve();
|
|
286
|
+
return {
|
|
287
|
+
agent: {
|
|
288
|
+
session: owner.getSession,
|
|
289
|
+
listSessions: owner.listSessions,
|
|
290
|
+
createSession: owner.createSession,
|
|
291
|
+
deleteSession: owner.deleteSession,
|
|
292
|
+
extensions: owner.extensions,
|
|
293
|
+
close: release,
|
|
294
|
+
},
|
|
295
|
+
release,
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function close() {
|
|
301
|
+
stopping = true;
|
|
302
|
+
closing ??= (async () => {
|
|
303
|
+
await Promise.allSettled(pendingOpenings);
|
|
304
|
+
const settled = await Promise.allSettled(opening);
|
|
305
|
+
const failures = [];
|
|
306
|
+
for (const result of settled) {
|
|
307
|
+
if (result.status === "fulfilled") {
|
|
308
|
+
try {
|
|
309
|
+
await result.value.runtime.close();
|
|
310
|
+
}
|
|
311
|
+
catch (cause) {
|
|
312
|
+
logEvent("service-owner.close-runtime", cause);
|
|
313
|
+
failures.push(cause);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
owners.clear();
|
|
318
|
+
opening.clear();
|
|
319
|
+
pendingOpenings.clear();
|
|
320
|
+
if (failures.length > 0)
|
|
321
|
+
throw new AggregateError(failures, "Local service owners failed to close");
|
|
322
|
+
})();
|
|
323
|
+
return closing;
|
|
324
|
+
}
|
|
325
|
+
async function restore() {
|
|
326
|
+
if (recoveryPath === undefined)
|
|
327
|
+
return;
|
|
328
|
+
let content;
|
|
329
|
+
try {
|
|
330
|
+
content = await readFile(recoveryPath, "utf8");
|
|
331
|
+
}
|
|
332
|
+
catch (cause) {
|
|
333
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
334
|
+
return;
|
|
335
|
+
logEvent("service-owner.read-recovery", cause);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const decoded = decodeRecoveryFile(content);
|
|
339
|
+
if (decoded._tag === "Failure") {
|
|
340
|
+
logEvent("service-owner.decode-recovery", decoded.failure);
|
|
341
|
+
await saveRecovery();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const saved = [];
|
|
345
|
+
for (const entry of decoded.success) {
|
|
346
|
+
const selection = recoveryOwnerSchema.safeParse(entry);
|
|
347
|
+
if (!selection.success) {
|
|
348
|
+
logEvent("service-owner.decode-recovery-entry", selection.error);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
saved.push(selection.data);
|
|
352
|
+
recovery.set(selection.data.databasePath, selection.data);
|
|
353
|
+
}
|
|
354
|
+
// Preload valid references before opening, then isolate and prune each
|
|
355
|
+
// failed owner so an unavailable project cannot prevent service startup.
|
|
356
|
+
for (const selection of saved) {
|
|
357
|
+
try {
|
|
358
|
+
await ownerFor(selection);
|
|
359
|
+
}
|
|
360
|
+
catch (cause) {
|
|
361
|
+
logEvent("service-owner.restore-entry", cause);
|
|
362
|
+
// A good owner may already have replaced another stale reference to
|
|
363
|
+
// this store. Prune only the failing reference, never that live owner.
|
|
364
|
+
if (recovery.get(selection.databasePath) === selection)
|
|
365
|
+
recovery.delete(selection.databasePath);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
await saveRecovery();
|
|
369
|
+
}
|
|
370
|
+
return { createSession, select, restore, close };
|
|
371
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { serviceDirectory } from "./private-local-service-discovery.js";
|
|
2
|
+
import { runPrivateLocalService } from "./private-local-service-listener.js";
|
|
3
|
+
const directory = process.argv[2];
|
|
4
|
+
if (!directory)
|
|
5
|
+
throw new Error("Local service requires its private directory");
|
|
6
|
+
await runPrivateLocalService(await serviceDirectory(directory));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { LocalSessionPathConflict } from "./private-local-service-owner.js";
|
|
2
|
+
import type { ShiitakeEventSourceConstructor } from "@fungi.computer/shiitake/client";
|
|
3
|
+
import type { PrivateLocalServiceManager } from "./private-local-service-contract.js";
|
|
4
|
+
export type { PrivateLocalServiceStatus, PrivateLocalServiceStopResult, PrivateLocalServiceSelection, PrivateLocalSessionCreation, PrivateLocalServiceConnection, PrivateLocalServiceManager, PrivateLocalServiceRequestOptions, PrivateLocalSessionChoice, PrivateLocalSessionDiscovery, PrivateLocalSessionInspection, PrivateLocalSessionProblem, } from "./private-local-service-contract.js";
|
|
5
|
+
export type { PrivateLocalModelChoice } from "./local-models.js";
|
|
6
|
+
export type { PrivateLocalEffectiveModel } from "./private-local-effective-model.js";
|
|
7
|
+
export { runPrivateLocalWorkerServer, type PrivateLocalWorkerServerIo, } from "./private-local-worker-server.js";
|
|
8
|
+
/** Construction does not start a service or open a store. Call start/connect explicitly. */
|
|
9
|
+
export declare function createPrivateLocalServiceManager(options?: {
|
|
10
|
+
directory?: string;
|
|
11
|
+
eventSource?: ShiitakeEventSourceConstructor;
|
|
12
|
+
executable?: string;
|
|
13
|
+
/** Test/client override; production captures the invoking client's PATH. */
|
|
14
|
+
executionPath?: string;
|
|
15
|
+
/** How long to wait for a live service that misses its handshake (default 60 s). */
|
|
16
|
+
busyServiceWaitMs?: number;
|
|
17
|
+
}): PrivateLocalServiceManager;
|