@fungi.computer/truffle 0.4.0 → 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.
@@ -37,6 +37,11 @@ const manifestSchema = z.strictObject({
37
37
  id: identifier.max(64),
38
38
  name: z.string().trim().min(1).max(64),
39
39
  description: z.string().max(200).default(""),
40
+ /** A shell key that opens this program beside the focused pane. */
41
+ binding: z
42
+ .string()
43
+ .regex(/^<leader>[a-z]$/u)
44
+ .optional(),
40
45
  /** Package-relative executable and its arguments. */
41
46
  command: z
42
47
  .array(z.string().min(1).max(1_024))
@@ -60,6 +65,7 @@ const terminalRecord = z.strictObject({
60
65
  id: z.string(),
61
66
  name: z.string(),
62
67
  description: z.string(),
68
+ binding: z.string().optional(),
63
69
  command: z.array(z.string()),
64
70
  });
65
71
  const revisionRecord = z.strictObject({
@@ -614,8 +620,8 @@ export function localExtensions(options) {
614
620
  if (record.client !== null)
615
621
  clients.push(record.client);
616
622
  const runtime = await runtimeDirectory(project, id);
617
- for (const terminal of record.terminals)
618
- terminals.push({
623
+ for (const terminal of record.terminals) {
624
+ const launcher = {
619
625
  extensionId: id,
620
626
  terminalId: terminal.id,
621
627
  name: terminal.name,
@@ -630,7 +636,11 @@ export function localExtensions(options) {
630
636
  FUNGI_PROJECT_ROOT: project,
631
637
  FUNGI_EXTENSION_RUNTIME_DIR: runtime,
632
638
  },
633
- });
639
+ };
640
+ terminals.push(terminal.binding === undefined
641
+ ? launcher
642
+ : { ...launcher, binding: terminal.binding });
643
+ }
634
644
  }
635
645
  return { clients, terminals, diagnostics };
636
646
  },
@@ -1,3 +1,4 @@
1
+ import { interactionTitleLimit } from "@fungi.computer/whistle/wire";
1
2
  /** Display order for popular model providers; labels remain owned by Pi. */
2
3
  export const localPopularProviderIds = Object.freeze([
3
4
  "fungi",
@@ -54,13 +55,32 @@ function notice(event) {
54
55
  return { type: "progress", message: event.message };
55
56
  }
56
57
  }
58
+ /**
59
+ * Pi's prompt text is provider wording, not a modal title: Whistle titles are
60
+ * at most `interactionTitleLimit` characters, and Pi 1.3's Anthropic paste
61
+ * prompt is longer. A message that fits stays the title; a longer one keeps
62
+ * every word as the summary under Fungi's fixed title for that prompt kind,
63
+ * so a provider sentence can never make the dialog refuse to open.
64
+ */
65
+ function modalText(message, fallbackTitle, hint) {
66
+ const fits = message.trim() !== "" && message.length <= interactionTitleLimit;
67
+ const lines = [
68
+ ...(fits ? [] : [message]),
69
+ ...(hint === undefined ? [] : [hint]),
70
+ ];
71
+ const title = fits ? message : fallbackTitle;
72
+ return lines.length === 0
73
+ ? { message: title }
74
+ : { message: title, summary: lines.join(" ") };
75
+ }
76
+ const placeholderOf = (placeholder) => placeholder === undefined ? {} : { placeholder };
57
77
  async function ask(interaction, prompt) {
58
78
  const options = prompt.signal === undefined ? {} : { signal: prompt.signal };
59
79
  switch (prompt.type) {
60
80
  case "select":
61
81
  return interaction.prompt({
62
82
  type: "choice",
63
- message: prompt.message,
83
+ ...modalText(prompt.message, "Choose a sign-in option"),
64
84
  options: prompt.options.map((option) => option.description === undefined
65
85
  ? { value: option.id, label: option.label }
66
86
  : {
@@ -69,36 +89,24 @@ async function ask(interaction, prompt) {
69
89
  description: option.description,
70
90
  }),
71
91
  }, options);
72
- case "manual_code": {
92
+ case "manual_code":
73
93
  // Generic across every provider that falls back to a pasted code or
74
94
  // redirect URL (OpenAI, Anthropic): the provider's own
75
95
  // message explains what to paste; this adds the SSH hint once, here,
76
96
  // instead of baking it into one provider's registry label.
77
- const summary = "Connecting over SSH? Paste the URL here instead of opening a browser.";
78
- const { message } = prompt;
79
- return interaction.prompt(prompt.placeholder === undefined
80
- ? { type: "text", message, summary }
81
- : {
82
- type: "text",
83
- message,
84
- summary,
85
- placeholder: prompt.placeholder,
86
- }, options);
87
- }
97
+ return interaction.prompt({
98
+ type: "text",
99
+ ...modalText(prompt.message, "Paste the sign-in code or redirect URL", "Connecting over SSH? Paste the URL here instead of opening a browser."),
100
+ ...placeholderOf(prompt.placeholder),
101
+ }, options);
88
102
  case "secret":
89
103
  case "text":
90
- return interaction.prompt(prompt.placeholder === undefined
91
- ? {
92
- type: "text",
93
- message: prompt.message,
94
- secret: prompt.type === "secret",
95
- }
96
- : {
97
- type: "text",
98
- message: prompt.message,
99
- placeholder: prompt.placeholder,
100
- secret: prompt.type === "secret",
101
- }, options);
104
+ return interaction.prompt({
105
+ type: "text",
106
+ ...modalText(prompt.message, prompt.type === "secret" ? "Enter your key" : "Enter a value"),
107
+ ...placeholderOf(prompt.placeholder),
108
+ secret: prompt.type === "secret",
109
+ }, options);
102
110
  }
103
111
  }
104
112
  const methodNotOffered = "The provider did not offer that sign-in method";
@@ -36,11 +36,13 @@ export type LocalArchiveRequest = Readonly<{
36
36
  * creating Agent, so the directory owner can hold the created Agent to what
37
37
  * the creator holds; its archive names who archives.
38
38
  */
39
- export type LocalAgentControlDirectory = Omit<AgentControlDirectory, "create" | "list" | "archive" | "unarchive"> & Readonly<{
39
+ export type LocalAgentControlDirectory = Omit<AgentControlDirectory, "create" | "list" | "archive" | "unarchive" | "answer" | "escalate"> & Readonly<{
40
40
  list: (signal: AbortSignal, options?: Readonly<{
41
41
  archived?: boolean;
42
42
  }>) => Promise<readonly LocalAgentControlAgent[]>;
43
43
  create: (input: LocalCreateRequest, signal: AbortSignal) => Promise<AgentControlAgent>;
44
+ answer: (input: Parameters<NonNullable<AgentControlDirectory["answer"]>>[0], senderAgentId: string, senderSessionId: string, signal: AbortSignal) => ReturnType<NonNullable<AgentControlDirectory["answer"]>>;
45
+ escalate: (input: Parameters<NonNullable<AgentControlDirectory["escalate"]>>[0], senderAgentId: string, senderSessionId: string, signal: AbortSignal) => ReturnType<NonNullable<AgentControlDirectory["escalate"]>>;
44
46
  archive: (input: LocalArchiveRequest, signal: AbortSignal) => Promise<void>;
45
47
  unarchive: (input: LocalArchiveRequest, signal: AbortSignal) => Promise<void>;
46
48
  }>;
@@ -75,6 +77,8 @@ export declare function createPrivateLocalAgentControl(input: Readonly<{
75
77
  }>) => Promise<readonly AgentControlAgent[]>;
76
78
  send?: (input: import("@fungi.computer/shiitake/agent-control").AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: import("@fungi.computer/shiitake/agent-control").AgentDelegationInvocation) => Promise<import("@fungi.computer/shiitake").Accepted | import("@fungi.computer/shiitake").Held>;
77
79
  recall?: (input: import("@fungi.computer/shiitake/agent-control").AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake").CancelResult>;
80
+ answer?: (input: import("@fungi.computer/shiitake/agent-control").AgentControlAnswerInput, senderSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake/agent-control").AgentControlAnswerOutput>;
81
+ escalate?: (input: import("@fungi.computer/shiitake/agent-control").AgentControlEscalateInput, senderSessionId: string, signal: AbortSignal) => Promise<import("@fungi.computer/shiitake/agent-control").AgentControlEscalateOutput>;
78
82
  create?: (input: Readonly<{
79
83
  requestId: string;
80
84
  name?: string;
@@ -8,7 +8,7 @@ import { createConversationAgents } from "@fungi.computer/shiitake-extensions/ro
8
8
  export function createPrivateLocalAgentControl(input) {
9
9
  const conversations = createConversationAgents(input.storage);
10
10
  function bound(parentSessionId) {
11
- const { create, archive, unarchive } = input.directory;
11
+ const { create, archive, unarchive, answer, escalate } = input.directory;
12
12
  const by = { kind: "agent", agentId: input.agentId };
13
13
  return {
14
14
  ...input.directory,
@@ -23,6 +23,16 @@ export function createPrivateLocalAgentControl(input) {
23
23
  createdBySessionId: parentSessionId,
24
24
  }, signal);
25
25
  },
26
+ answer: (request, sourceSessionId, signal) => parentSessionId === undefined
27
+ ? Promise.resolve({ status: "refused", reason: "no_provenance" })
28
+ : sourceSessionId !== parentSessionId
29
+ ? Promise.resolve({ status: "refused", reason: "not_sender" })
30
+ : answer(request, input.agentId, parentSessionId, signal),
31
+ escalate: (request, sourceSessionId, signal) => parentSessionId === undefined
32
+ ? Promise.resolve({ status: "refused", reason: "no_provenance" })
33
+ : sourceSessionId !== parentSessionId
34
+ ? Promise.resolve({ status: "refused", reason: "not_sender" })
35
+ : escalate(request, input.agentId, parentSessionId, signal),
26
36
  };
27
37
  }
28
38
  const control = conversations.control(bound);
@@ -87,8 +87,13 @@ declare const wireResultSchema: z.ZodUnion<readonly [z.ZodDiscriminatedUnion<[z.
87
87
  category: z.ZodLiteral<"cancelled">;
88
88
  }, z.core.$strict>, z.ZodObject<{
89
89
  category: z.ZodLiteral<"no-key-entered">;
90
+ }, z.core.$strict>, z.ZodObject<{
91
+ category: z.ZodLiteral<"prompt-failed">;
92
+ }, z.core.$strict>, z.ZodObject<{
93
+ category: z.ZodLiteral<"missing-authorization-code">;
90
94
  }, z.core.$strict>, z.ZodObject<{
91
95
  category: z.ZodLiteral<"unknown">;
96
+ errorName: z.ZodString;
92
97
  }, z.core.$strict>], "category">;
93
98
  }, z.core.$strict>]>;
94
99
  declare class AuthPromptTarget extends RpcTarget {
@@ -80,6 +80,24 @@ const eventSchema = z.discriminatedUnion("type", [
80
80
  }),
81
81
  ]);
82
82
  const answerSchema = z.string().max(localAuthInputLimit);
83
+ const errorNameSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,63}$/u);
84
+ /** A class name only: anything else (or a non-Error) is named generically. */
85
+ function errorNameOf(cause) {
86
+ if (!(cause instanceof Error))
87
+ return "NonError";
88
+ const name = errorNameSchema.safeParse(cause.name);
89
+ return name.success ? name.data : "Error";
90
+ }
91
+ /**
92
+ * The person's own dialog could not show or answer a provider question (for
93
+ * example the prompt failed the renderer's contract). Fixed text, no cause.
94
+ */
95
+ class LocalAuthPromptFailed extends Error {
96
+ constructor() {
97
+ super("Authentication prompt failed");
98
+ this.name = "LocalAuthPromptFailed";
99
+ }
100
+ }
83
101
  /**
84
102
  * The closed set of failures that cross the service. Provider errors can
85
103
  * contain credential material, so no free text, cause or provider message
@@ -98,7 +116,13 @@ const localAuthFailureSchema = z.discriminatedUnion("category", [
98
116
  }),
99
117
  z.strictObject({ category: z.literal("cancelled") }),
100
118
  z.strictObject({ category: z.literal("no-key-entered") }),
101
- z.strictObject({ category: z.literal("unknown") }),
119
+ z.strictObject({ category: z.literal("prompt-failed") }),
120
+ z.strictObject({ category: z.literal("missing-authorization-code") }),
121
+ z.strictObject({
122
+ category: z.literal("unknown"),
123
+ /** The thrown value's class name only, e.g. `TypeError`; never its message. */
124
+ errorName: errorNameSchema,
125
+ }),
102
126
  ]);
103
127
  const wireResultSchema = z.union([
104
128
  authResultSchema,
@@ -110,6 +134,7 @@ const wireResultSchema = z.union([
110
134
  /** Pi 1.3.0's fixed error prefixes; never retain a provider body suffix. */
111
135
  const callbackPortInUse = /^Port \d+ is in use\b/u;
112
136
  const oauthStateMismatch = "OAuth state mismatch";
137
+ const missingAuthorizationCode = "Missing authorization code";
113
138
  const openAITokenRequestFailed = /^OpenAI OAuth token request failed \(([1-5]\d{2})\)/u;
114
139
  function classifyFailure(cause, aborted) {
115
140
  if (aborted)
@@ -133,7 +158,14 @@ function classifyFailure(cause, aborted) {
133
158
  return { category: "callback-port-in-use" };
134
159
  if (cause instanceof Error && cause.message === oauthStateMismatch)
135
160
  return { category: "oauth-state-mismatch" };
136
- return { category: "unknown" };
161
+ if (cause instanceof LocalAuthPromptFailed)
162
+ return { category: "prompt-failed" };
163
+ if (cause instanceof Error && cause.message === missingAuthorizationCode)
164
+ return { category: "missing-authorization-code" };
165
+ return {
166
+ category: "unknown",
167
+ errorName: errorNameOf(cause),
168
+ };
137
169
  }
138
170
  /** Fixed wording per category; the dialog shows exactly this. */
139
171
  function failureText(failure) {
@@ -164,8 +196,12 @@ function failureText(failure) {
164
196
  return "Authentication cancelled";
165
197
  case "no-key-entered":
166
198
  return new LocalAuthRefusal().message;
199
+ case "prompt-failed":
200
+ return "The sign-in dialog could not show the provider's question (prompt-failed). Nothing was saved.";
201
+ case "missing-authorization-code":
202
+ return "No authorization code was found in what was pasted. Paste the code or the full redirect URL.";
167
203
  case "unknown":
168
- return "Local authentication failed";
204
+ return `Local authentication failed (unknown: ${failure.errorName}). Nothing was saved.`;
169
205
  }
170
206
  }
171
207
  /** The client-side error for one typed failure; its message is fixed text. */
@@ -333,7 +369,13 @@ export class PrivateLocalAuthTarget extends RpcTarget {
333
369
  try {
334
370
  if (promptSignal.aborted)
335
371
  cancel();
336
- return answerSchema.parse(await pending.result());
372
+ const answer = await pending.result().then((value) => ({ value }), (cause) => ({ cause }));
373
+ if ("cause" in answer) {
374
+ promptSignal.throwIfAborted();
375
+ // The presenter's error text is not trusted to be secret-free.
376
+ throw new LocalAuthPromptFailed();
377
+ }
378
+ return answerSchema.parse(answer.value);
337
379
  }
338
380
  finally {
339
381
  promptSignal.removeEventListener("abort", cancel);
@@ -5,7 +5,9 @@ import { basename, dirname, join } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { z } from "zod";
7
7
  const execute = promisify(execFile);
8
- const queryOptions = { timeout: 2_000, maxBuffer: 4_096 };
8
+ // A compiled launcher cold-starts a Bun process; 2s sat at twice its quiet-box
9
+ // time and killed the query on a loaded box.
10
+ const queryOptions = { timeout: 10_000, maxBuffer: 4_096 };
9
11
  const buildId = z.string().regex(/^[a-f0-9]{64}$/u);
10
12
  function installedLauncherOnPath() {
11
13
  return new Promise((resolve, reject) => {
@@ -28,9 +28,27 @@ export declare function createPrivateLocalDelegation(input: Readonly<{
28
28
  name: string;
29
29
  }[] | undefined>) | undefined;
30
30
  accept: (mail: BirdDogMail, meta?: BirdDogMail["meta"], reply?: VerifiedAgentReply) => Promise<Accepted>;
31
+ /**
32
+ * What happens to items a child left waiting on this Agent when the work
33
+ * that was told about them ends. Without it a host leaves them to expire.
34
+ */
35
+ unattended?: Readonly<{
36
+ /** Exact settled facts of one command of this Agent's own Sessions. */
37
+ readSettlement: (sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
38
+ /**
39
+ * Pass one child item on to a person, as this Agent's Session that was
40
+ * told about it. A child that already moved on answers that and nothing changes.
41
+ */
42
+ escalate: (item: Readonly<{
43
+ agentId: string;
44
+ sessionId: string;
45
+ itemId: string;
46
+ }>, receiverSessionId: string) => Promise<void>;
47
+ }>;
31
48
  }>): {
32
49
  send: (task: AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: AgentDelegationInvocation) => Promise<DelegationReceipt>;
33
50
  recall: (task: AgentRecallInput, sourceSessionId: string, signal: AbortSignal) => Promise<CancelResult>;
34
51
  receive: (mail: BirdDogMail) => Promise<Accepted>;
52
+ close: () => Promise<void>;
35
53
  outstanding: () => Promise<import("@fungi.computer/shiitake/fleet").OutstandingDelegation | undefined>;
36
54
  };
@@ -1,3 +1,4 @@
1
+ import * as Effect from "effect/Effect";
1
2
  import { AgentArchiveRefused } from "@fungi.computer/shiitake/agent-control";
2
3
  import { agentDelegationRequestKey, createFleetDelegation, delegationProvenance, delegationReplyMeta, delegationSendMeta, } from "@fungi.computer/shiitake/fleet";
3
4
  /** How long one Local delegation grant stays valid while it is admitted. */
@@ -31,7 +32,67 @@ export function createPrivateLocalDelegation(input) {
31
32
  }
32
33
  return agents;
33
34
  }
34
- /** Send with one caller-owned identity; follow-up replies return here. */
35
+ /**
36
+ * Items a child left waiting on this Agent, by the reply command that told
37
+ * it. When the work that carried the reply ends, whatever the Agent neither
38
+ * answered nor escalated goes to a person, so no waiting item is silent.
39
+ */
40
+ const unattended = new Map();
41
+ let poll;
42
+ /** Escalate the items of every reply whose carrying work has settled. */
43
+ async function settleUnattended() {
44
+ const handling = input.unattended;
45
+ if (handling === undefined)
46
+ return;
47
+ for (const [commandId, entry] of unattended) {
48
+ const settlement = await handling
49
+ .readSettlement(entry.sessionId, commandId)
50
+ .catch((cause) => {
51
+ // Not settled as far as anyone can tell; try again on the next poll.
52
+ Effect.runFork(Effect.logWarning("delegation.settlement", cause));
53
+ return undefined;
54
+ });
55
+ if (settlement === undefined)
56
+ continue;
57
+ unattended.delete(commandId);
58
+ for (const itemId of entry.items)
59
+ await handling
60
+ .escalate({ ...entry.child, itemId }, entry.sessionId)
61
+ .catch((cause) => {
62
+ Effect.runFork(Effect.logWarning("delegation.escalate", cause));
63
+ });
64
+ }
65
+ if (unattended.size === 0 && poll !== undefined) {
66
+ clearInterval(poll);
67
+ poll = undefined;
68
+ }
69
+ }
70
+ function watchUnattended(accepted, receiverSessionId, reply) {
71
+ if (input.unattended === undefined ||
72
+ reply.outcome === "undeliverable" ||
73
+ reply.waitingOnYou === undefined)
74
+ return;
75
+ if (receiverSessionId === undefined)
76
+ return;
77
+ unattended.set(accepted.commandId, {
78
+ sessionId: receiverSessionId,
79
+ child: { agentId: reply.agentId, sessionId: reply.sessionId },
80
+ items: reply.waitingOnYou.map(({ itemId }) => itemId),
81
+ });
82
+ poll ??= setInterval(() => {
83
+ void settleUnattended();
84
+ }, 1_000);
85
+ poll.unref();
86
+ }
87
+ /** Escalate what is owed now and stop watching; the Agent is closing. */
88
+ async function close() {
89
+ await settleUnattended();
90
+ unattended.clear();
91
+ if (poll !== undefined)
92
+ clearInterval(poll);
93
+ poll = undefined;
94
+ }
95
+ /** Send with one caller-owned identity; follow-up and steer replies return here. */
35
96
  async function send(task, sourceSessionId, signal, invocation) {
36
97
  if (invocation === undefined)
37
98
  throw new Error("Delegation requires a tool invocation");
@@ -43,19 +104,9 @@ export function createPrivateLocalDelegation(input) {
43
104
  requestId: task.requestId,
44
105
  });
45
106
  signal.throwIfAborted();
46
- if (task.delivery === "steer") {
47
- const opened = await input.directory.open(task.agentId, signal);
48
- try {
49
- return await opened.agent.session(task.sessionId).send(task.text, {
50
- requestId,
51
- delivery: "steer",
52
- });
53
- }
54
- finally {
55
- await opened.release();
56
- }
57
- }
58
- const meta = delegationSendMeta({ id: input.agentId, name: input.agentId }, invocation.meta);
107
+ // A person reads this name where work waits on its sender.
108
+ const sender = agents.find(({ agentId }) => agentId === input.agentId);
109
+ const meta = delegationSendMeta({ id: input.agentId, name: sender?.name ?? input.agentId }, invocation.meta);
59
110
  // The caller's request key owns replay, not the current model program's bytes.
60
111
  const provenance = await delegationProvenance({
61
112
  agentId: input.agentId,
@@ -71,6 +122,7 @@ export function createPrivateLocalDelegation(input) {
71
122
  }, {
72
123
  agentId: task.agentId,
73
124
  sessionId: task.sessionId,
125
+ delivery: task.delivery,
74
126
  text: task.text,
75
127
  requestId,
76
128
  });
@@ -147,7 +199,9 @@ export function createPrivateLocalDelegation(input) {
147
199
  if (replier === undefined && !authenticatedUndeliverable)
148
200
  throw new Error("Local reply sender is outside this project");
149
201
  const meta = delegationReplyMeta(delegation, replier?.name ?? delegation.reply.agentId);
150
- return input.accept({ ...mail, meta }, meta, delegation.reply);
202
+ const accepted = await input.accept({ ...mail, meta }, meta, delegation.reply);
203
+ watchUnattended(accepted, mail.to.sessionId, delegation.reply);
204
+ return accepted;
151
205
  }
152
- return { send, recall, receive, outstanding: fleet.outstanding };
206
+ return { send, recall, receive, close, outstanding: fleet.outstanding };
153
207
  }
@@ -46,7 +46,7 @@ import { createPrivateInvocationFilesPort, } from "./private-invocation-files.js
46
46
  import { createLocalSkillSource } from "./local-skill-source.js";
47
47
  import { createPrivateWorkerdSandbox } from "./private-workerd-sandbox.js";
48
48
  import { createPrivateFungiMcpConnection, } from "./private-fungi-mcp-connection.js";
49
- import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, } from "./private-sqlite-owner.js";
49
+ import { createPrivateSqliteOwner, openPrivateWritableSqliteDatabase, storeWriteBusyTimeoutMs, } from "./private-sqlite-owner.js";
50
50
  import { createPrivateLocalMailRoom, hasPrivateLocalPendingMail, } from "./private-local-mail.js";
51
51
  import { createPrivateLocalDelegation, } from "./private-local-delegation.js";
52
52
  import { createPrivateLocalAgentControl, } from "./private-local-agent-control.js";
@@ -207,6 +207,30 @@ async function assertPrivateLocalReceiptFile(path, kind) {
207
207
  throw cause;
208
208
  }
209
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
+ }
210
234
  /** Resolve an Agent create request through its durable Local receipt. */
211
235
  export async function resolvePrivateLocalCreateStore(root, requestId, creator) {
212
236
  if (requestId.length === 0 || requestId.length > 256)
@@ -229,27 +253,13 @@ export async function resolvePrivateLocalCreateStore(root, requestId, creator) {
229
253
  try {
230
254
  database.exec(`PRAGMA busy_timeout = ${String(receiptBusyTimeoutMs)}`);
231
255
  database.exec("BEGIN IMMEDIATE");
232
- database.exec(`
233
- CREATE TABLE IF NOT EXISTS ${localCreateReceiptTable} (
234
- request_id TEXT PRIMARY KEY NOT NULL,
235
- agent_id TEXT NOT NULL UNIQUE,
236
- name TEXT,
237
- created_by_agent_id TEXT,
238
- created_by_session_id TEXT
239
- ) WITHOUT ROWID
240
- `);
241
- const receiptColumns = database
242
- .prepare(`PRAGMA table_info(${localCreateReceiptTable})`)
243
- .all()
244
- .map((column) => column.name);
245
- if (!receiptColumns.includes("created_by_agent_id"))
246
- database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_agent_id TEXT`);
247
- if (!receiptColumns.includes("created_by_session_id"))
248
- database.exec(`ALTER TABLE ${localCreateReceiptTable} ADD COLUMN created_by_session_id TEXT`);
249
- const existing = database
250
- .prepare(`SELECT agent_id, created_by_agent_id, created_by_session_id FROM ${localCreateReceiptTable} WHERE request_id = ?`)
251
- .get(requestId);
256
+ ensureReceiptTable(database);
257
+ const existing = readReceiptRow(database, requestId);
252
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");
253
263
  if (creatorAgentId !== undefined &&
254
264
  (existing.created_by_agent_id !== creatorAgentId ||
255
265
  existing.created_by_session_id !== creatorSessionId))
@@ -616,6 +626,13 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
616
626
  message.stopReason === "error" &&
617
627
  modelFailure?.message === message &&
618
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,
619
636
  failureReason: (message) => modelFailure?.message === message ? modelFailure.reason : undefined,
620
637
  providerError: (message) => modelFailure?.message === message
621
638
  ? modelFailure.providerError
@@ -637,7 +654,7 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
637
654
  const evidence = {
638
655
  refusal: turn.refusal(),
639
656
  upstream: upstream ?? partial,
640
- rejection: provider.rejection(),
657
+ rejection: provider.rejection(upstream),
641
658
  };
642
659
  const message = cause === undefined
643
660
  ? modelFailureMessage(selected.provider, evidence)
@@ -674,6 +691,8 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
674
691
  reason: message,
675
692
  retryable: modelFailureRetryable(evidence),
676
693
  };
694
+ if (evidence.rejection !== undefined)
695
+ modelFailure.rejection = evidence.rejection;
677
696
  if (upstream?.errorMessage !== undefined)
678
697
  modelFailure.providerError = upstream.errorMessage;
679
698
  }
@@ -1044,7 +1063,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
1044
1063
  providerCacheKey = createHash("sha256")
1045
1064
  .update(JSON.stringify(["fungi-local", paths.databasePath, initialSessionId]))
1046
1065
  .digest("hex");
1047
- database = openPrivateWritableSqliteDatabase(paths.databasePath, 0);
1066
+ database = openPrivateWritableSqliteDatabase(paths.databasePath, storeWriteBusyTimeoutMs);
1048
1067
  database.exec("PRAGMA foreign_keys = ON");
1049
1068
  const agentId = ensureLocalAgentId({
1050
1069
  database,
@@ -1119,6 +1138,14 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
1119
1138
  return Promise.reject(new Error("Local mail ingress is unavailable"));
1120
1139
  return acceptMail(mail, meta, reply);
1121
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
+ },
1122
1149
  });
1123
1150
  let projectDirectory;
1124
1151
  if (input.agentControl !== undefined) {
@@ -1531,7 +1558,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
1531
1558
  };
1532
1559
  },
1533
1560
  close: () => {
1534
- closing ??= closePrivateLocalResources({
1561
+ closing ??= (delegation?.close() ?? Promise.resolve()).then(() => closePrivateLocalResources({
1535
1562
  agent,
1536
1563
  processes,
1537
1564
  sandbox,
@@ -1539,7 +1566,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
1539
1566
  database,
1540
1567
  lease,
1541
1568
  providerCacheKey,
1542
- });
1569
+ }));
1543
1570
  return closing;
1544
1571
  },
1545
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;