@fungi.computer/truffle 0.3.1 → 0.5.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.
Files changed (41) hide show
  1. package/dist/local-board-agents.d.ts +46 -0
  2. package/dist/local-board-agents.js +138 -0
  3. package/dist/local-board-shipping.d.ts +129 -0
  4. package/dist/local-board-shipping.js +325 -0
  5. package/dist/local-extensions.d.ts +2 -0
  6. package/dist/local-extensions.js +121 -36
  7. package/dist/local-login.js +33 -19
  8. package/dist/local-upgrade.d.ts +63 -0
  9. package/dist/local-upgrade.js +358 -0
  10. package/dist/private-local-agent-control.d.ts +8 -1
  11. package/dist/private-local-agent-control.js +16 -2
  12. package/dist/private-local-auth-rpc.d.ts +5 -0
  13. package/dist/private-local-auth-rpc.js +46 -4
  14. package/dist/private-local-client-version.js +3 -1
  15. package/dist/private-local-delegation.d.ts +18 -0
  16. package/dist/private-local-delegation.js +82 -21
  17. package/dist/private-local-exec-review.js +45 -8
  18. package/dist/private-local-extension-archive-notice.d.ts +8 -0
  19. package/dist/private-local-extension-archive-notice.js +108 -0
  20. package/dist/private-local-extension-records.d.ts +25 -0
  21. package/dist/private-local-extension-records.js +51 -0
  22. package/dist/private-local-process-identity.d.ts +14 -0
  23. package/dist/private-local-process-identity.js +51 -0
  24. package/dist/private-local-runtime-host.d.ts +7 -1
  25. package/dist/private-local-runtime-host.js +88 -31
  26. package/dist/private-local-service-contract.d.ts +1 -0
  27. package/dist/private-local-service-discovery.js +2 -3
  28. package/dist/private-local-service-owner.js +80 -9
  29. package/dist/private-local-service-termination.js +12 -11
  30. package/dist/private-local-service.js +19 -1
  31. package/dist/private-local-session-discovery.d.ts +16 -4
  32. package/dist/private-local-session-discovery.js +68 -4
  33. package/dist/private-mcp-form-contract.d.ts +13 -0
  34. package/dist/private-mcp-form-contract.js +6 -1
  35. package/dist/private-mcp-form-session.d.ts +26 -0
  36. package/dist/private-mcp-form-session.js +8 -5
  37. package/dist/private-provider-rejection.d.ts +1 -1
  38. package/dist/private-provider-rejection.js +28 -8
  39. package/dist/private-sqlite-owner.d.ts +7 -1
  40. package/dist/private-sqlite-owner.js +14 -2
  41. package/package.json +32 -16
@@ -6,8 +6,9 @@ import { logEvent } from "./log-event.js";
6
6
  import { localArchiveHandoff } from "./local-archive-handoff.js";
7
7
  import { createMcpFormSession } from "./private-mcp-form-session.js";
8
8
  import { localExecEscapeReview } from "./private-local-exec-review.js";
9
- import { ApprovalLedger, } from "@fungi.computer/approvals";
9
+ import { ApprovalLedger, denialFeedbackOpening, } from "@fungi.computer/approvals";
10
10
  import { localExtensions, } from "./local-extensions.js";
11
+ import { deferExtensionArchiveNotices } from "./private-local-extension-archive-notice.js";
11
12
  import { lstat, mkdir, realpath, stat } from "node:fs/promises";
12
13
  import { lstatSync, mkdirSync, realpathSync } from "node:fs";
13
14
  import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
@@ -19,7 +20,7 @@ import { parseModelPreference, } from "@fungi.computer/shiitake/model-preference
19
20
  import { AgentCreateRefused, createAgentControlSource, } from "@fungi.computer/shiitake/agent-control";
20
21
  import { defineIdFamily } from "@fungi.computer/cairn";
21
22
  import { words } from "@fungi.computer/cairn/words";
22
- import { botanicalExtensionsWithSkills } from "@fungi.computer/shiitake-extensions/roster";
23
+ import { botanicalExtensionsWithSkills, botanicalExtensionsWithSubagents, } from "@fungi.computer/shiitake-extensions/roster";
23
24
  import { homeExtension, homeGrant, } from "@fungi.computer/shiitake-extensions/home/host";
24
25
  import { Knapsack } from "@fungi.computer/knapsack";
25
26
  import { makeHost, Shiitake, ShiitakeError, } from "@fungi.computer/shiitake";
@@ -45,11 +46,11 @@ import { createPrivateInvocationFilesPort, } from "./private-invocation-files.js
45
46
  import { createLocalSkillSource } from "./local-skill-source.js";
46
47
  import { createPrivateWorkerdSandbox } from "./private-workerd-sandbox.js";
47
48
  import { createPrivateFungiMcpConnection, } from "./private-fungi-mcp-connection.js";
48
- import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, } from "./private-sqlite-owner.js";
49
+ import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, storeWriteBusyTimeoutMs, } from "./private-sqlite-owner.js";
49
50
  import { createPrivateLocalMailRoom, hasPrivateLocalPendingMail, } from "./private-local-mail.js";
50
51
  import { createPrivateLocalDelegation, } from "./private-local-delegation.js";
51
52
  import { createPrivateLocalAgentControl, } from "./private-local-agent-control.js";
52
- import { subagentsExtension, subagentsGrant, } from "@fungi.computer/shiitake-extensions/subagents/host";
53
+ import {} from "@fungi.computer/shiitake-extensions/subagents/host";
53
54
  const initialSessionId = "main";
54
55
  const maximumInstructionReadBytes = 128 * 1_024;
55
56
  const maximumInstructionSnapshotBytes = 32_000;
@@ -206,10 +207,40 @@ async function assertPrivateLocalReceiptFile(path, kind) {
206
207
  throw cause;
207
208
  }
208
209
  }
210
+ function ensureReceiptTable(database) {
211
+ database.exec(`
212
+ CREATE TABLE IF NOT EXISTS ${localCreateReceiptTable} (
213
+ request_id TEXT PRIMARY KEY NOT NULL,
214
+ agent_id TEXT NOT NULL UNIQUE,
215
+ name TEXT,
216
+ created_by_agent_id TEXT,
217
+ created_by_session_id TEXT
218
+ ) WITHOUT ROWID
219
+ `);
220
+ const receiptColumns = database
221
+ .prepare(`PRAGMA table_info(${localCreateReceiptTable})`)
222
+ .all()
223
+ .map((column) => column.name);
224
+ if (!receiptColumns.includes("created_by_agent_id"))
225
+ database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_agent_id TEXT`);
226
+ if (!receiptColumns.includes("created_by_session_id"))
227
+ database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_session_id TEXT`);
228
+ }
229
+ function readReceiptRow(database, requestId) {
230
+ return database
231
+ .prepare(`SELECT agent_id, created_by_agent_id, created_by_session_id FROM ${localCreateReceiptTable} WHERE request_id = ?`)
232
+ .get(requestId);
233
+ }
209
234
  /** Resolve an Agent create request through its durable Local receipt. */
210
- export async function resolvePrivateLocalCreateStore(root, requestId) {
235
+ export async function resolvePrivateLocalCreateStore(root, requestId, creator) {
211
236
  if (requestId.length === 0 || requestId.length > 256)
212
237
  throw new Error("Local Agent create request id is invalid");
238
+ const creatorAgentId = creator === undefined
239
+ ? undefined
240
+ : z.string().min(1).max(256).parse(creator.agentId);
241
+ const creatorSessionId = creator === undefined
242
+ ? undefined
243
+ : z.string().min(1).max(256).parse(creator.sessionId);
213
244
  await ensurePrivateLocalGitExclude(root);
214
245
  const fungiDirectory = join(root, ".fungi");
215
246
  const agentsDirectory = join(fungiDirectory, "agents");
@@ -222,17 +253,17 @@ export async function resolvePrivateLocalCreateStore(root, requestId) {
222
253
  try {
223
254
  database.exec(`PRAGMA busy_timeout = ${String(receiptBusyTimeoutMs)}`);
224
255
  database.exec("BEGIN IMMEDIATE");
225
- database.exec(`
226
- CREATE TABLE IF NOT EXISTS ${localCreateReceiptTable} (
227
- request_id TEXT PRIMARY KEY NOT NULL,
228
- agent_id TEXT NOT NULL UNIQUE,
229
- name TEXT
230
- ) WITHOUT ROWID
231
- `);
232
- const existing = database
233
- .prepare(`SELECT agent_id FROM ${localCreateReceiptTable} WHERE request_id = ?`)
234
- .get(requestId);
256
+ ensureReceiptTable(database);
257
+ const existing = readReceiptRow(database, requestId);
235
258
  if (existing !== undefined) {
259
+ if (creatorAgentId !== undefined &&
260
+ (existing.created_by_agent_id === null ||
261
+ existing.created_by_agent_id === undefined))
262
+ throw new AgentCreateRefused("request_conflict", "the Local Agent create receipt is unbound; use a fresh requestId");
263
+ if (creatorAgentId !== undefined &&
264
+ (existing.created_by_agent_id !== creatorAgentId ||
265
+ existing.created_by_session_id !== creatorSessionId))
266
+ throw new AgentCreateRefused("request_conflict", "the create request id is already bound to a different creating Agent or Session");
236
267
  const parsed = localHostIds.store.schema.safeParse(existing.agent_id);
237
268
  if (!parsed.success)
238
269
  throw new Error("Local Agent create receipt is invalid");
@@ -245,7 +276,7 @@ export async function resolvePrivateLocalCreateStore(root, requestId) {
245
276
  };
246
277
  }
247
278
  let agentId;
248
- const insert = database.prepare(`INSERT OR IGNORE INTO ${localCreateReceiptTable} (request_id, agent_id) VALUES (?, ?)`);
279
+ const insert = database.prepare(`INSERT OR IGNORE INTO ${localCreateReceiptTable} (request_id, agent_id, created_by_agent_id, created_by_session_id) VALUES (?, ?, ?, ?)`);
249
280
  for (let attempt = 0; attempt < maximumReceiptIdAttempts; attempt += 1) {
250
281
  const candidate = localHostIds.store.generate();
251
282
  const candidateDirectory = join(agentsDirectory, candidate);
@@ -267,7 +298,7 @@ export async function resolvePrivateLocalCreateStore(root, requestId) {
267
298
  metadata.isSymbolicLink() ||
268
299
  realpathSync(candidateDirectory) !== candidateDirectory)
269
300
  throw new Error("Local Agent create directory is not project owned");
270
- if (insert.run(requestId, candidate).changes > 0) {
301
+ if (insert.run(requestId, candidate, creatorAgentId ?? null, creatorSessionId ?? null).changes > 0) {
271
302
  agentId = candidate;
272
303
  break;
273
304
  }
@@ -295,7 +326,7 @@ export async function resolvePrivateLocalCreateStore(root, requestId) {
295
326
  database.close();
296
327
  }
297
328
  }
298
- async function localDatabasePaths(root, requested, createRequestId) {
329
+ async function localDatabasePaths(root, requested, createRequestId, creator) {
299
330
  if (requested !== undefined && createRequestId !== undefined)
300
331
  throw new Error("Local Agent database and create request are mutually exclusive");
301
332
  if (requested !== undefined) {
@@ -311,7 +342,7 @@ async function localDatabasePaths(root, requested, createRequestId) {
311
342
  }
312
343
  const createStore = createRequestId === undefined
313
344
  ? undefined
314
- : await resolvePrivateLocalCreateStore(root, createRequestId);
345
+ : await resolvePrivateLocalCreateStore(root, createRequestId, creator);
315
346
  const createDatabasePath = createStore?.databasePath;
316
347
  const generatedAgentId = createStore?.agentId ?? localHostIds.store.generate();
317
348
  const selectedParent = createDatabasePath === undefined
@@ -595,6 +626,13 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
595
626
  message.stopReason === "error" &&
596
627
  modelFailure?.message === message &&
597
628
  modelFailure.retryable,
629
+ retryAfterMs: (message) => modelFailure?.message === message &&
630
+ modelFailure.rejection?.code === "rate-limit"
631
+ ? modelFailure.rejection.retryAfterMs
632
+ : undefined,
633
+ rejection: (message) => modelFailure?.message === message
634
+ ? modelFailure.rejection
635
+ : undefined,
598
636
  failureReason: (message) => modelFailure?.message === message ? modelFailure.reason : undefined,
599
637
  providerError: (message) => modelFailure?.message === message
600
638
  ? modelFailure.providerError
@@ -616,7 +654,7 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
616
654
  const evidence = {
617
655
  refusal: turn.refusal(),
618
656
  upstream: upstream ?? partial,
619
- rejection: provider.rejection(),
657
+ rejection: provider.rejection(upstream),
620
658
  };
621
659
  const message = cause === undefined
622
660
  ? modelFailureMessage(selected.provider, evidence)
@@ -653,6 +691,8 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
653
691
  reason: message,
654
692
  retryable: modelFailureRetryable(evidence),
655
693
  };
694
+ if (evidence.rejection !== undefined)
695
+ modelFailure.rejection = evidence.rejection;
656
696
  if (upstream?.errorMessage !== undefined)
657
697
  modelFailure.providerError = upstream.errorMessage;
658
698
  }
@@ -778,6 +818,7 @@ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox,
778
818
  : [
779
819
  createPrivateLocalComputerSource(root, port, sessionPath, processes, configRoot.home, sandbox?.launch, currentMode, escapeForSession, policyForSession),
780
820
  ];
821
+ deferExtensionArchiveNotices(configRoot.home);
781
822
  const installed = configuration?.installed ??
782
823
  (storeDirectory === undefined
783
824
  ? { roster: [], grants: [], skills: [], diagnostics: [] }
@@ -793,9 +834,12 @@ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox,
793
834
  const archive = storeDirectory === undefined
794
835
  ? { roster: [], grants: [] }
795
836
  : localArchiveHandoff(storeDirectory, installed.grants);
837
+ const composed = subagents === undefined
838
+ ? base
839
+ : botanicalExtensionsWithSubagents(base, subagents.source);
796
840
  const extensions = {
797
841
  roster: [
798
- ...base.roster,
842
+ ...composed.roster,
799
843
  forms.extension,
800
844
  homeExtension({
801
845
  computers: {
@@ -808,20 +852,16 @@ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox,
808
852
  ],
809
853
  },
810
854
  }),
811
- ...(subagents === undefined
812
- ? []
813
- : [subagentsExtension({ source: subagents.source })]),
814
855
  ...archive.roster,
815
856
  ...installed.roster,
816
857
  ],
817
858
  grants: [
818
- ...base.grants,
859
+ ...composed.grants,
819
860
  {
820
861
  id: forms.extension.manifest.id,
821
862
  capabilities: forms.extension.manifest.requests,
822
863
  },
823
864
  homeGrant,
824
- ...(subagents === undefined ? [] : [subagentsGrant]),
825
865
  ...archive.grants,
826
866
  ...installed.grants,
827
867
  ],
@@ -1002,7 +1042,12 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
1002
1042
  if (reason !== undefined)
1003
1043
  throw new LocalSandboxUnavailable(reason);
1004
1044
  }
1005
- const paths = await localDatabasePaths(root, input.databasePath, input.createRequestId);
1045
+ const paths = await localDatabasePaths(root, input.databasePath, input.createRequestId, input.createdBy === undefined || input.createdBySessionId === undefined
1046
+ ? undefined
1047
+ : {
1048
+ agentId: input.createdBy,
1049
+ sessionId: input.createdBySessionId,
1050
+ });
1006
1051
  const runtime = localRuntimeOwner();
1007
1052
  let lease;
1008
1053
  let providerCacheKey;
@@ -1018,7 +1063,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
1018
1063
  providerCacheKey = createHash("sha256")
1019
1064
  .update(JSON.stringify(["fungi-local", paths.databasePath, initialSessionId]))
1020
1065
  .digest("hex");
1021
- database = openPrivateWritableSqliteDatabase(paths.databasePath, 0);
1066
+ database = openPrivateWritableSqliteDatabase(paths.databasePath, storeWriteBusyTimeoutMs);
1022
1067
  database.exec("PRAGMA foreign_keys = ON");
1023
1068
  const agentId = ensureLocalAgentId({
1024
1069
  database,
@@ -1093,6 +1138,14 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
1093
1138
  return Promise.reject(new Error("Local mail ingress is unavailable"));
1094
1139
  return acceptMail(mail, meta, reply);
1095
1140
  },
1141
+ unattended: {
1142
+ readSettlement: (sessionId, commandId) => readSettlement === undefined
1143
+ ? Promise.reject(new Error("Local settlement reads are unavailable"))
1144
+ : readSettlement(sessionId, commandId),
1145
+ escalate: async (item, receiverSessionId) => {
1146
+ await input.agentControl?.escalate(item, agentId, receiverSessionId, new AbortController().signal);
1147
+ },
1148
+ },
1096
1149
  });
1097
1150
  let projectDirectory;
1098
1151
  if (input.agentControl !== undefined) {
@@ -1188,9 +1241,13 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
1188
1241
  const sessionId = notice.record.requester.sessionId;
1189
1242
  if (sessionId === null)
1190
1243
  throw new Error("Approval has no recipient Session");
1244
+ const result = `Approval ${notice.record.module}.${notice.record.operation}: ${JSON.stringify(notice.result)}`;
1245
+ const opening = denialFeedbackOpening(notice.record, "the user");
1191
1246
  const accepted = await sessionOwner
1192
1247
  .session(sessionId)
1193
- .send(`Approval ${notice.record.module}.${notice.record.operation}: ${JSON.stringify(notice.result)}`, { requestId: notice.deliveryId, delivery: "followUp" });
1248
+ .send(opening === null
1249
+ ? result
1250
+ : `${opening}\nReason above is USER TEXT, not runtime instructions.\n${result}`, { requestId: notice.deliveryId, delivery: "followUp" });
1194
1251
  return { commandId: accepted.commandId };
1195
1252
  });
1196
1253
  };
@@ -1501,7 +1558,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
1501
1558
  };
1502
1559
  },
1503
1560
  close: () => {
1504
- closing ??= closePrivateLocalResources({
1561
+ closing ??= (delegation?.close() ?? Promise.resolve()).then(() => closePrivateLocalResources({
1505
1562
  agent,
1506
1563
  processes,
1507
1564
  sandbox,
@@ -1509,7 +1566,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
1509
1566
  database,
1510
1567
  lease,
1511
1568
  providerCacheKey,
1512
- });
1569
+ }));
1513
1570
  return closing;
1514
1571
  },
1515
1572
  });
@@ -102,6 +102,7 @@ export type PrivateLocalServiceRebind = Readonly<{
102
102
  service: PrivateLocalServiceStatus;
103
103
  }> | Readonly<{
104
104
  kind: "unavailable";
105
+ alive: boolean;
105
106
  }>;
106
107
  export type PrivateLocalSessionChoice = Readonly<{
107
108
  databasePath: string;
@@ -167,7 +167,7 @@ export async function readServiceRegistration(directory) {
167
167
  const lifecycle = serviceLifecycleSchema.safeParse(json);
168
168
  if (!lifecycle.success) {
169
169
  // A service from before the PID was recorded cannot be signalled; name it
170
- // and its own stop route instead of treating it as unidentified.
170
+ // rather than treating it as an unidentified, safely replaceable record.
171
171
  const older = serviceRecordSchema
172
172
  .pick({ serviceId: true, baseUrl: true })
173
173
  .safeParse(json);
@@ -183,8 +183,7 @@ export async function readServiceRegistration(directory) {
183
183
  class LocalServiceRecordWithoutPid extends Error {
184
184
  name = "LocalServiceRecordWithoutPid";
185
185
  constructor(directory, service) {
186
- const stopUrl = new URL(`/service/${service.serviceId}/stop`, service.baseUrl);
187
- super(`An older Fungi service (${service.serviceId} on port ${new URL(service.baseUrl).port}) is registered in ${recordPath(directory)}, and this fungi cannot stop it. Stop it with \`curl -X POST ${stopUrl.href}\`; if it keeps running, end the process whose command line contains \`--internal-service ${directory}\`. If it is no longer running, delete ${recordPath(directory)}. Then retry; saved Sessions are kept.`);
186
+ super(`An older Fungi service (${service.serviceId} on port ${new URL(service.baseUrl).port}) is registered in ${recordPath(directory)} without a process identity. Fungi cannot safely signal it. End the process whose command line contains \`--internal-service ${directory}\`. If it is no longer running, move ${recordPath(directory)} aside. Then retry; saved Sessions are kept.`);
188
187
  }
189
188
  }
190
189
  /** The record of a service speaking this client's protocol, if one is registered. */
@@ -10,18 +10,20 @@ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
10
10
  import { setTimeout as delay } from "node:timers/promises";
11
11
  import { AgentCreateRefused, AgentArchiveRefused, } from "@fungi.computer/shiitake/agent-control";
12
12
  import { localExtensions, LocalExtensionError, } from "./local-extensions.js";
13
+ import { deferExtensionArchiveNotices } from "./private-local-extension-archive-notice.js";
13
14
  import { readFile, realpath } from "node:fs/promises";
14
15
  import writeFileAtomic from "write-file-atomic";
15
16
  import { z } from "zod";
16
17
  import * as Schema from "effect/Schema";
17
18
  import { selectionSchema } from "./private-local-service-discovery.js";
18
19
  import { createShiitakeHttpRoutes } from "@fungi.computer/shiitake/http";
20
+ import { questionContract } from "@fungi.computer/shiitake-extensions/question";
19
21
  import { listPrivateLocalAgentStores, LocalStoreSchemaError, readPrivateLocalAgentProfile, readPrivateLocalSessionSummaries, isArchivedStore, unreadableStoreReason, } from "./private-local-session-discovery.js";
20
22
  import { createPrivateLocalSessionRequestId, openPrivateLocalRuntimeHost, resolvePrivateLocalCreateStore, } from "./private-local-runtime-host.js";
21
23
  import { parsePrivateLocalExecPath } from "./private-local-exec-path.js";
22
24
  import { createLocalMeshLinks, localMeshLinkKey, } from "./private-local-mesh-links.js";
23
25
  import { agentMailAddressSchema, machineAgentRequestSchema, machineDirectorySchema, } from "@fungi.computer/seam/machine-link-protocol";
24
- import { commandSettlementSchema } from "@fungi.computer/shiitake/fleet";
26
+ import { commandSettlementSchema, settlementReadableBy, } from "@fungi.computer/shiitake/fleet";
25
27
  import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
26
28
  export class LocalSessionPathConflict extends Error {
27
29
  name = "LocalSessionPathConflict";
@@ -72,7 +74,11 @@ readRecoveryManifest = (path) => readFile(path, "utf8"),
72
74
  onRecoveryDisabled = () => undefined, configRoot = capturePrivateLocalConfigRoot(), meshLinksFactory = createLocalMeshLinks, onExecRuleMatch = () => undefined,
73
75
  /** Supplied by the listener after startup; individual Agents never close it. */
74
76
  serviceNative, onExecApprovalDecision = () => undefined) {
75
- const extensions = suppliedExtensions ?? localExtensions({ home: configRoot.home });
77
+ deferExtensionArchiveNotices(configRoot.home);
78
+ const extensions = suppliedExtensions ??
79
+ localExtensions({
80
+ home: configRoot.home,
81
+ });
76
82
  const recoveryPath = recoveryDirectory === undefined
77
83
  ? undefined
78
84
  : resolve(recoveryDirectory, "owners.json");
@@ -158,7 +164,7 @@ serviceNative, onExecApprovalDecision = () => undefined) {
158
164
  return { status: "accepted", receipt: { sessionId } };
159
165
  }
160
166
  const facts = await admitted(owner, () => owner.readSettlement(operation.sessionId, operation.commandId));
161
- if (facts !== undefined && facts.acceptedSender !== actorAgentId)
167
+ if (facts !== undefined && !settlementReadableBy(facts, actorAgentId))
162
168
  return "refused";
163
169
  return {
164
170
  status: "accepted",
@@ -176,8 +182,9 @@ serviceNative, onExecApprovalDecision = () => undefined) {
176
182
  envelope.key !== mail.key ||
177
183
  mail.to.agentId !== address.agentId)
178
184
  return "refused";
179
- if (mail.from.sessionId === undefined &&
180
- !(mail.replyTo.length === 0 && mail.reply?.outcome === "undeliverable"))
185
+ if (mail.from.kind === "system" &&
186
+ !(mail.reply?.outcome === "undeliverable" &&
187
+ mail.reply.reason === "recipient_offline"))
181
188
  return "refused";
182
189
  const sessionlessAcceptance = mail.to.sessionId === undefined;
183
190
  // Only select the physical store here. Never bypass that runtime's
@@ -229,7 +236,9 @@ serviceNative, onExecApprovalDecision = () => undefined) {
229
236
  for (const project of projects)
230
237
  selections.set(project, await readProjectFungiMcpSelection(project));
231
238
  const candidate = {
232
- load: await localExtensions({ home: configRoot.home }).captureAgent(),
239
+ load: await localExtensions({
240
+ home: configRoot.home,
241
+ }).captureAgent(),
233
242
  mcp: selections,
234
243
  clientPath: path,
235
244
  };
@@ -801,8 +810,9 @@ serviceNative, onExecApprovalDecision = () => undefined) {
801
810
  const control = directory(invocationRoot);
802
811
  return {
803
812
  deliver: async (mail) => {
813
+ const senderAgentId = mail.from.kind === "agent" ? mail.from.agentId : undefined;
804
814
  const remote = [...meshDirectories.values()].find(({ record }) => record.invocationRoot === invocationRoot &&
805
- record.bindings.some((binding) => binding.agentId === mail.from.agentId));
815
+ record.bindings.some((binding) => binding.agentId === senderAgentId));
806
816
  if (remote !== undefined) {
807
817
  if (!remote.agents.some((agent) => agent.agentId === mail.to.agentId))
808
818
  throw new Error("Mesh mail recipient is outside this Team");
@@ -893,6 +903,62 @@ serviceNative, onExecApprovalDecision = () => undefined) {
893
903
  throw new AgentArchiveRefused("archived", `Agent ${agentId} is archived; unarchive it to work with it`);
894
904
  throw new Error("Agent is outside this Local project");
895
905
  }
906
+ /**
907
+ * Run one sender-authenticated command on a child's question owner. Which
908
+ * Agent the item waits on is decided by the child's own store; this door
909
+ * only states who is calling.
910
+ */
911
+ async function callQuestionAsSender(target, senderAgentId, senderSessionId, command, commandInput, output, signal) {
912
+ ensureRunning();
913
+ signal.throwIfAborted();
914
+ const databasePath = (await targets(signal, "all")).get(target.agentId);
915
+ if (databasePath === undefined)
916
+ return outsideOrArchived(target.agentId, signal);
917
+ const { owner, release } = await admittedOwner(target.agentId, signal);
918
+ try {
919
+ return await admitted(owner, async () => Schema.decodeUnknownSync(output)(await owner
920
+ .getSession(target.sessionId)
921
+ .extension("botanical-session-question")
922
+ .call(command, commandInput, {
923
+ agentId: senderAgentId,
924
+ sessionId: senderSessionId,
925
+ })));
926
+ }
927
+ finally {
928
+ release();
929
+ }
930
+ }
931
+ async function answerAsSender(input, senderAgentId, senderSessionId, signal) {
932
+ const decoded = await callQuestionAsSender(input, senderAgentId, senderSessionId, "answerAgent", {
933
+ askId: input.askId,
934
+ requestId: input.requestId,
935
+ answers: input.answers,
936
+ }, questionContract.commands.answerAgent.output, signal);
937
+ const { result } = decoded;
938
+ if (result.result === "accepted")
939
+ return result.commandId === undefined
940
+ ? { status: "answered", via: result.via }
941
+ : {
942
+ status: "answered",
943
+ via: result.via,
944
+ commandId: result.commandId,
945
+ };
946
+ if (result.result === "closed")
947
+ return { status: "closed", reason: result.status };
948
+ if (result.result === "refused")
949
+ return { status: "refused", reason: result.reason };
950
+ return { status: "not_found" };
951
+ }
952
+ async function escalateAsSender(input, senderAgentId, senderSessionId, signal) {
953
+ const decoded = await callQuestionAsSender(input, senderAgentId, senderSessionId, "escalateAgent", { askId: input.itemId }, questionContract.commands.escalateAgent.output, signal);
954
+ if (decoded.result === "escalated")
955
+ return { status: "escalated", to: decoded.to };
956
+ if (decoded.result === "closed")
957
+ return { status: "closed", reason: decoded.status };
958
+ if (decoded.result === "refused")
959
+ return { status: "refused", reason: decoded.reason };
960
+ return { status: "not_found" };
961
+ }
896
962
  /**
897
963
  * The first command still queued or running on any of an Agent's Sessions,
898
964
  * so an archive can name it rather than cancel it.
@@ -969,6 +1035,8 @@ serviceNative, onExecApprovalDecision = () => undefined) {
969
1035
  }
970
1036
  return {
971
1037
  owner: admittedOwner,
1038
+ answer: answerAsSender,
1039
+ escalate: escalateAsSender,
972
1040
  archive: (request, signal) => setArchived(request, true, signal),
973
1041
  unarchive: (request, signal) => setArchived(request, false, signal),
974
1042
  list: async (signal, options) => {
@@ -1001,9 +1069,10 @@ serviceNative, onExecApprovalDecision = () => undefined) {
1001
1069
  return model === null ? agent : { ...agent, model };
1002
1070
  });
1003
1071
  },
1004
- create: async ({ requestId, name, model, extensions: requested = [], createdBy }, signal) => {
1072
+ create: async ({ requestId, name, model, extensions: requested = [], createdBy, createdBySessionId, }, signal) => {
1005
1073
  ensureRunning();
1006
1074
  signal.throwIfAborted();
1075
+ z.string().min(1).max(256).parse(createdBySessionId);
1007
1076
  // An Agent creating an Agent cannot give it more than it holds. Refuse
1008
1077
  // before anything exists; never trim the request.
1009
1078
  const creatorDatabase = (await targets(signal)).get(createdBy);
@@ -1021,7 +1090,8 @@ serviceNative, onExecApprovalDecision = () => undefined) {
1021
1090
  throw error;
1022
1091
  }
1023
1092
  const key = `create:${JSON.stringify([invocationRoot, requestId])}`;
1024
- const createStore = await resolvePrivateLocalCreateStore(invocationRoot, requestId);
1093
+ const creator = { agentId: createdBy, sessionId: createdBySessionId };
1094
+ const createStore = await resolvePrivateLocalCreateStore(invocationRoot, requestId, creator);
1025
1095
  signal.throwIfAborted();
1026
1096
  ensureRunning();
1027
1097
  const databasePath = createStore.databasePath;
@@ -1045,6 +1115,7 @@ serviceNative, onExecApprovalDecision = () => undefined) {
1045
1115
  createRequestId: requestId,
1046
1116
  name,
1047
1117
  createdBy,
1118
+ createdBySessionId,
1048
1119
  mode: creatorProfile.mode,
1049
1120
  folder: creatorProfile.folder,
1050
1121
  agentControl: directory(invocationRoot),
@@ -1,5 +1,5 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
- import { readProcessStart } from "./private-local-process-identity.js";
2
+ import { identifyLocalServiceProcess } from "./private-local-process-identity.js";
3
3
  import { readServiceLeaseOwner, } from "./private-local-service-lease.js";
4
4
  /** How long a SIGTERM'd service may drain its owners before it is killed. */
5
5
  const terminationGraceMs = 5_000;
@@ -8,11 +8,11 @@ const terminationPollMs = 50;
8
8
  * Whether `service` is still the process running under its PID. A PID whose
9
9
  * start differs is refused. A separate read and signal still has a race window.
10
10
  */
11
- async function isServiceProcess(service) {
12
- const current = await readProcessStart(service.pid);
13
- if (current.kind === "unverifiable")
14
- throw new Error(`Fungi cannot verify the service process ${String(service.pid)} with sub-second precision on ${process.platform}, so it refuses to signal it. Close that service manually, then reopen Fungi.`);
15
- return current.kind === "running" && current.start === service.startedAt;
11
+ async function isServiceProcess(directory, service) {
12
+ const current = await identifyLocalServiceProcess({ ...service, directory });
13
+ if (current.kind === "other" && process.platform !== "linux")
14
+ throw new Error(`Cannot safely stop Fungi service pid ${String(service.pid)} in ${directory}: its command line does not identify this service. No process was signalled.`);
15
+ return current.kind === "ours";
16
16
  }
17
17
  /** Signal `pid`; false when it has already exited. */
18
18
  function signalService(pid, signal) {
@@ -33,12 +33,13 @@ async function signalIfStillRunning(directory, service, signal) {
33
33
  const owner = await readServiceLeaseOwner(directory);
34
34
  if (owner?.pid !== service.pid || owner.startedAt !== service.startedAt)
35
35
  return false;
36
- return (await isServiceProcess(owner)) && signalService(owner.pid, signal);
36
+ return ((await isServiceProcess(directory, owner)) &&
37
+ signalService(owner.pid, signal));
37
38
  }
38
- async function exitedWithin(service, ms) {
39
+ async function exitedWithin(directory, service, ms) {
39
40
  const deadline = Date.now() + ms;
40
41
  for (;;) {
41
- if (!(await isServiceProcess(service)))
42
+ if (!(await isServiceProcess(directory, service)))
42
43
  return true;
43
44
  if (Date.now() >= deadline)
44
45
  return false;
@@ -57,11 +58,11 @@ export async function terminatePrivateLocalService(directory, service) {
57
58
  throw new Error("The Fungi service runs inside this process; stop it from within instead of with fungi stop.");
58
59
  if (!(await signalIfStillRunning(directory, service, "SIGTERM")))
59
60
  return "stopped";
60
- if (await exitedWithin(service, terminationGraceMs))
61
+ if (await exitedWithin(directory, service, terminationGraceMs))
61
62
  return "stopped";
62
63
  if (!(await signalIfStillRunning(directory, service, "SIGKILL")))
63
64
  return "stopped";
64
- if (await exitedWithin(service, terminationGraceMs))
65
+ if (await exitedWithin(directory, service, terminationGraceMs))
65
66
  return "killed";
66
67
  throw new Error(`Local service process ${String(service.pid)} is still running after SIGKILL`);
67
68
  }
@@ -4,6 +4,7 @@ import { readBoundedResponse } from "./private-bounded-response.js";
4
4
  import { LocalSandboxUnavailable, sandboxUnavailableReasonSchema, } from "./private-local-containment.js";
5
5
  export { LocalSandboxUnavailable } from "./private-local-containment.js";
6
6
  import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
7
+ import { takePendingExtensionArchiveNotices } from "./private-local-extension-archive-notice.js";
7
8
  import { stopAllResultSchema } from "./private-local-stop-all.js";
8
9
  import { LocalAgentSelectionAmbiguous, LocalSessionPathConflict, } from "./private-local-service-owner.js";
9
10
  export { LocalAgentSelectionAmbiguous, LocalSessionPathConflict, } from "./private-local-service-owner.js";
@@ -111,6 +112,16 @@ export function createPrivateLocalServiceManager(options = {}) {
111
112
  ? capturePrivateLocalExecPath(process.env.PATH ?? "/usr/bin:/bin", process.cwd())
112
113
  : parsePrivateLocalExecPath(options.executionPath);
113
114
  const expectedVersion = privateLocalServiceVersion();
115
+ async function reportPendingExtensionArchiveNotices() {
116
+ try {
117
+ const notices = await takePendingExtensionArchiveNotices(root.home);
118
+ for (const notice of notices)
119
+ process.stderr.write(`${notice}\n`);
120
+ }
121
+ catch (cause) {
122
+ logEvent("extension.archive-notice", cause);
123
+ }
124
+ }
114
125
  /**
115
126
  * Set once this client has reached a service. Only a starting client
116
127
  * replaces a service speaking another protocol. Same-protocol build changes
@@ -344,6 +355,7 @@ export function createPrivateLocalServiceManager(options = {}) {
344
355
  request.signal?.throwIfAborted();
345
356
  const service = await start(request);
346
357
  const selected = await postSelection(endpoint, input, service, request.signal);
358
+ await reportPendingExtensionArchiveNotices();
347
359
  const firstBase = routeBase(service, selected);
348
360
  /**
349
361
  * Where this connection's client currently talks. A restarted service
@@ -454,7 +466,13 @@ export function createPrivateLocalServiceManager(options = {}) {
454
466
  // stopped it decides when it returns.
455
467
  const ready = await status();
456
468
  if (ready === undefined)
457
- return { kind: "unavailable" };
469
+ return {
470
+ kind: "unavailable",
471
+ // The discovery owner's process-lifetime lease distinguishes an
472
+ // overloaded listener from a stopped service without guessing
473
+ // from the failed HTTP probe or an error message.
474
+ alive: await serviceOwnerIsLive(selectedDirectory),
475
+ };
458
476
  accept(ready);
459
477
  const again = await postSelection("select", {
460
478
  invocationRoot: input.invocationRoot,
@@ -17,30 +17,41 @@ export declare function unreadableStoreReason(agentId: string): string;
17
17
  */
18
18
  export declare function isArchivedStore(path: string): boolean;
19
19
  export declare function listPrivateLocalAgentStores(root: string, visibility?: LocalStoreVisibility): Promise<readonly string[]>;
20
+ /** Entry-preserving survey: a symlink is archived as a link, not its target. */
21
+ export declare function probePrivateLocalAgentStoreEntries(home: string, maximum?: number): Promise<readonly Readonly<{
22
+ agentId: string;
23
+ entry: string;
24
+ databasePath: string;
25
+ }>[]>;
20
26
  /** A Local or Woodstock store outside the current schema; Local never migrates it. */
21
27
  export declare class LocalStoreSchemaError extends Error {
22
28
  readonly databasePath: string;
23
29
  readonly name = "LocalStoreSchemaError";
24
30
  constructor(databasePath: string, options?: ErrorOptions);
25
31
  }
32
+ export type PrivateLocalAgentProfile = ReturnType<typeof openAgentProfile>;
26
33
  /**
27
34
  * One Agent store's chosen display name, default model, whether it is
28
35
  * archived and the Agent that created it (absent when a human made it), read
29
- * without opening a host.
36
+ * without opening a host. Directory listings and the sub-agents footer read
37
+ * every store's profile on each call; an unchanged store file answers from
38
+ * memory instead of reopening SQLite (each Bun open/close also leaks native
39
+ * memory).
30
40
  */
31
- export declare function readPrivateLocalAgentProfile(path: string): {
41
+ export declare function readPrivateLocalAgentProfile(path: string): PrivateLocalAgentProfile;
42
+ declare function openAgentProfile(path: string): Readonly<{
32
43
  name: string | undefined;
33
44
  createdBy: string | null;
34
45
  archived: boolean;
35
46
  mode: "read-only" | "workspace" | "full-access";
36
- writableFolders: string[];
47
+ writableFolders: readonly string[];
37
48
  folder: string;
38
49
  model: {
39
50
  readonly providerId: string;
40
51
  readonly modelId: string;
41
52
  readonly reasoning?: "high" | "low" | "max" | "medium" | "minimal" | "xhigh" | undefined;
42
53
  } | null;
43
- };
54
+ }>;
44
55
  export declare function readPrivateLocalSessionSummaries(path: string): Promise<readonly LocalSummary[]>;
45
56
  /**
46
57
  * Discover existing folder sessions without opening or reconciling a host.
@@ -65,3 +76,4 @@ export declare function readPrivateLocalSessionDiscovery(invocationRoot: string,
65
76
  }>;
66
77
  /** Project the service-owned discovery into choices that resume through it. */
67
78
  export declare function projectPrivateLocalSessionDiscovery(discovered: Awaited<ReturnType<typeof readPrivateLocalSessionDiscovery>>, connect: PrivateLocalServiceManager["connect"]): PrivateLocalSessionDiscovery;
79
+ export {};