@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
@@ -1,3 +1,4 @@
1
+ import { statSync } from "node:fs";
1
2
  import { readdir, realpath, stat } from "node:fs/promises";
2
3
  import { basename, dirname, join, resolve } from "node:path";
3
4
  import * as Effect from "effect/Effect";
@@ -102,6 +103,37 @@ export async function listPrivateLocalAgentStores(root, visibility = "active") {
102
103
  }
103
104
  return [...paths];
104
105
  }
106
+ /** Entry-preserving survey: a symlink is archived as a link, not its target. */
107
+ export async function probePrivateLocalAgentStoreEntries(home, maximum = 512) {
108
+ const directory = join(home, ".fungi", "agents");
109
+ let entries;
110
+ try {
111
+ entries = await readdir(directory, { withFileTypes: true });
112
+ }
113
+ catch (cause) {
114
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
115
+ return [];
116
+ throw cause;
117
+ }
118
+ const stores = entries.filter((entry) => entry.isDirectory() || entry.isSymbolicLink());
119
+ if (stores.length > maximum)
120
+ throw new Error(`Fungi cannot survey ${String(stores.length)} Agent stores (maximum ${String(maximum)})`);
121
+ const candidates = [];
122
+ for (const store of stores) {
123
+ const entry = join(directory, store.name);
124
+ const databasePath = join(entry, "agent.sqlite");
125
+ try {
126
+ if ((await stat(databasePath)).isFile())
127
+ candidates.push({ agentId: store.name, entry, databasePath });
128
+ }
129
+ catch (cause) {
130
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
131
+ continue;
132
+ throw cause;
133
+ }
134
+ }
135
+ return candidates;
136
+ }
105
137
  const storedProfileSchema = z.strictObject({
106
138
  name: z.string().nullable(),
107
139
  createdBy: z.string().nullable(),
@@ -120,12 +152,44 @@ export class LocalStoreSchemaError extends Error {
120
152
  this.databasePath = databasePath;
121
153
  }
122
154
  }
155
+ /** Profiles held at once; directory listings touch every store each call. */
156
+ const cachedProfiles = 1_024;
157
+ const profiles = new Map();
158
+ /** Every committed write moves the store file (or its WAL); none rewrites it in place unseen. */
159
+ function storeVersion(path) {
160
+ const version = (file) => {
161
+ const metadata = statSync(file, { bigint: true, throwIfNoEntry: false });
162
+ return metadata === undefined
163
+ ? "-"
164
+ : `${String(metadata.dev)}:${String(metadata.ino)}:${String(metadata.size)}:${String(metadata.mtimeNs)}:${String(metadata.ctimeNs)}`;
165
+ };
166
+ return `${version(path)}/${version(`${path}-wal`)}`;
167
+ }
123
168
  /**
124
169
  * One Agent store's chosen display name, default model, whether it is
125
170
  * archived and the Agent that created it (absent when a human made it), read
126
- * without opening a host.
171
+ * without opening a host. Directory listings and the sub-agents footer read
172
+ * every store's profile on each call; an unchanged store file answers from
173
+ * memory instead of reopening SQLite (each Bun open/close also leaks native
174
+ * memory).
127
175
  */
128
176
  export function readPrivateLocalAgentProfile(path) {
177
+ const version = storeVersion(path);
178
+ const cached = profiles.get(path);
179
+ if (cached?.version === version)
180
+ return cached.profile;
181
+ const profile = openAgentProfile(path);
182
+ // The version is taken before the read, so a write racing it misses next time.
183
+ profiles.delete(path);
184
+ profiles.set(path, { version, profile });
185
+ if (profiles.size > cachedProfiles) {
186
+ const oldest = profiles.keys().next().value;
187
+ if (oldest !== undefined)
188
+ profiles.delete(oldest);
189
+ }
190
+ return profile;
191
+ }
192
+ function openAgentProfile(path) {
129
193
  const database = openPrivateReadOnlySqliteDatabase(path);
130
194
  try {
131
195
  const columns = database
@@ -153,15 +217,15 @@ export function readPrivateLocalAgentProfile(path) {
153
217
  folder: identity?.folder,
154
218
  model: readAgentDefaultModelJson(database),
155
219
  });
156
- return {
220
+ return Object.freeze({
157
221
  name: stored.name ?? undefined,
158
222
  createdBy: stored.createdBy,
159
223
  archived: stored.archived === 1,
160
224
  mode: stored.mode,
161
- writableFolders: stored.writableFolders,
225
+ writableFolders: Object.freeze(stored.writableFolders),
162
226
  folder: stored.folder,
163
227
  model: parseModelPreferenceJson(stored.model),
164
- };
228
+ });
165
229
  }
166
230
  finally {
167
231
  database.close();
@@ -51,6 +51,7 @@ declare const answerResult: Schema.Union<readonly [Schema.Struct<{
51
51
  readonly kind: Schema.Literal<"policy">;
52
52
  }>]>>;
53
53
  readonly reason: Schema.NullOr<Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>>;
54
+ readonly denialNote: Schema.optionalKey<Schema.String>;
54
55
  readonly requestedAt: Schema.Int;
55
56
  readonly expiresAt: Schema.Int;
56
57
  readonly updatedAt: Schema.Int;
@@ -62,6 +63,7 @@ declare const answerResult: Schema.Union<readonly [Schema.Struct<{
62
63
  }>, Schema.Struct<{
63
64
  readonly kind: Schema.Literal<"refused">;
64
65
  readonly reason: Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>;
66
+ readonly denialNote: Schema.optionalKey<Schema.String>;
65
67
  }>, Schema.Struct<{
66
68
  readonly kind: Schema.Literal<"submitted">;
67
69
  }>, Schema.Struct<{
@@ -79,6 +81,7 @@ export declare const mcpFormApprovalContract: {
79
81
  readonly id: Schema.String;
80
82
  readonly decision: Schema.Literals<readonly ["approve", "deny", "always-allow"]>;
81
83
  readonly expectedRevision: Schema.Int;
84
+ readonly denialNote: Schema.optionalKey<Schema.String>;
82
85
  }>;
83
86
  output: Schema.Union<readonly [Schema.Struct<{
84
87
  readonly kind: Schema.Literal<"answered">;
@@ -115,6 +118,7 @@ export declare const mcpFormApprovalContract: {
115
118
  readonly kind: Schema.Literal<"policy">;
116
119
  }>]>>;
117
120
  readonly reason: Schema.NullOr<Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>>;
121
+ readonly denialNote: Schema.optionalKey<Schema.String>;
118
122
  readonly requestedAt: Schema.Int;
119
123
  readonly expiresAt: Schema.Int;
120
124
  readonly updatedAt: Schema.Int;
@@ -126,6 +130,7 @@ export declare const mcpFormApprovalContract: {
126
130
  }>, Schema.Struct<{
127
131
  readonly kind: Schema.Literal<"refused">;
128
132
  readonly reason: Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>;
133
+ readonly denialNote: Schema.optionalKey<Schema.String>;
129
134
  }>, Schema.Struct<{
130
135
  readonly kind: Schema.Literal<"submitted">;
131
136
  }>, Schema.Struct<{
@@ -171,6 +176,7 @@ export declare const mcpFormApprovalContract: {
171
176
  readonly kind: Schema.Literal<"policy">;
172
177
  }>]>>;
173
178
  readonly reason: Schema.NullOr<Schema.Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>>;
179
+ readonly denialNote: Schema.optionalKey<Schema.String>;
174
180
  readonly requestedAt: Schema.Int;
175
181
  readonly expiresAt: Schema.Int;
176
182
  readonly updatedAt: Schema.Int;
@@ -183,10 +189,17 @@ export declare const mcpFormApprovalContract: {
183
189
  readonly command: Schema.String;
184
190
  readonly cwd: Schema.String;
185
191
  readonly justification: Schema.String;
192
+ readonly form: Schema.Literals<readonly ["direct", "shell"]>;
193
+ readonly kind: Schema.Literals<readonly ["exec", "extension-install"]>;
194
+ readonly argv: Schema.optionalKey<Schema.$Array<Schema.String>>;
186
195
  readonly prefix: Schema.optionalKey<Schema.$Array<Schema.String>>;
187
196
  readonly warning: Schema.optionalKey<Schema.String>;
188
197
  readonly alwaysAllowCoverage: Schema.optionalKey<Schema.String>;
189
198
  readonly alwaysAllowUnavailableReason: Schema.optionalKey<Schema.String>;
199
+ readonly risk: Schema.optionalKey<Schema.Struct<{
200
+ readonly summary: Schema.String;
201
+ readonly detail: Schema.String;
202
+ }>>;
190
203
  }>>;
191
204
  }>>;
192
205
  }>;
@@ -1,6 +1,6 @@
1
1
  import { ExtensionId, } from "@fungi.computer/shiitake/extensions";
2
2
  import * as Schema from "effect/Schema";
3
- import { ApprovalAnswerInputSchema, ApprovalRefusalSchema, ApprovalViewSchema, } from "@fungi.computer/approvals";
3
+ import { ApprovalAnswerInputSchema, ApprovalDenialNoteSchema, ApprovalRefusalSchema, ApprovalViewSchema, } from "@fungi.computer/approvals";
4
4
  /** The actual MCP form, not a ledger record or an authorization decision. */
5
5
  export const McpFormApprovalRequestSchema = Schema.Struct({
6
6
  message: Schema.String,
@@ -36,10 +36,14 @@ const McpFormPendingSchema = Schema.Struct({
36
36
  command: Schema.String,
37
37
  cwd: Schema.String,
38
38
  justification: Schema.String,
39
+ form: Schema.Literals(["direct", "shell"]),
40
+ kind: Schema.Literals(["exec", "extension-install"]),
41
+ argv: Schema.optionalKey(Schema.Array(Schema.String)),
39
42
  prefix: Schema.optionalKey(Schema.Array(Schema.String)),
40
43
  warning: Schema.optionalKey(Schema.String),
41
44
  alwaysAllowCoverage: Schema.optionalKey(Schema.String),
42
45
  alwaysAllowUnavailableReason: Schema.optionalKey(Schema.String),
46
+ risk: Schema.optionalKey(Schema.Struct({ summary: Schema.String, detail: Schema.String })),
43
47
  })),
44
48
  });
45
49
  /** The existing Session extension slot carries only display-safe form data. */
@@ -52,6 +56,7 @@ export const mcpFormApprovalContract = {
52
56
  id: Schema.String,
53
57
  decision: Schema.Literals(["approve", "deny", "always-allow"]),
54
58
  expectedRevision: ApprovalAnswerInputSchema.fields.expectedRevision,
59
+ denialNote: Schema.optionalKey(ApprovalDenialNoteSchema),
55
60
  }),
56
61
  output: answerResult,
57
62
  },
@@ -5,10 +5,17 @@ export type LocalApprovalReview = {
5
5
  command: string;
6
6
  cwd: string;
7
7
  justification: string;
8
+ form: "direct" | "shell";
9
+ kind: "exec" | "extension-install";
10
+ argv?: string[];
8
11
  prefix?: readonly string[];
9
12
  warning?: string;
10
13
  alwaysAllowCoverage?: string;
11
14
  alwaysAllowUnavailableReason?: string;
15
+ risk?: {
16
+ summary: string;
17
+ detail: string;
18
+ };
12
19
  };
13
20
  type Pending = {
14
21
  sessionId: string;
@@ -27,6 +34,7 @@ export declare function createMcpFormSession(): {
27
34
  readonly id: import("effect/Schema").String;
28
35
  readonly decision: import("effect/Schema").Literals<readonly ["approve", "deny", "always-allow"]>;
29
36
  readonly expectedRevision: import("effect/Schema").Int;
37
+ readonly denialNote: import("effect/Schema").optionalKey<import("effect/Schema").String>;
30
38
  }>;
31
39
  output: import("effect/Schema").Union<readonly [import("effect/Schema").Struct<{
32
40
  readonly kind: import("effect/Schema").Literal<"answered">;
@@ -63,6 +71,7 @@ export declare function createMcpFormSession(): {
63
71
  readonly kind: import("effect/Schema").Literal<"policy">;
64
72
  }>]>>;
65
73
  readonly reason: import("effect/Schema").NullOr<import("effect/Schema").Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>>;
74
+ readonly denialNote: import("effect/Schema").optionalKey<import("effect/Schema").String>;
66
75
  readonly requestedAt: import("effect/Schema").Int;
67
76
  readonly expiresAt: import("effect/Schema").Int;
68
77
  readonly updatedAt: import("effect/Schema").Int;
@@ -74,6 +83,7 @@ export declare function createMcpFormSession(): {
74
83
  }>, import("effect/Schema").Struct<{
75
84
  readonly kind: import("effect/Schema").Literal<"refused">;
76
85
  readonly reason: import("effect/Schema").Literals<readonly ["not-found", "not-eligible", "already-answered", "stale-revision", "expired", "declined", "fenced", "not-granted", "read-only", "pending", "in-flight", "outcome-unknown", "identity-conflict"]>;
86
+ readonly denialNote: import("effect/Schema").optionalKey<import("effect/Schema").String>;
77
87
  }>, import("effect/Schema").Struct<{
78
88
  readonly kind: import("effect/Schema").Literal<"submitted">;
79
89
  }>, import("effect/Schema").Struct<{
@@ -115,6 +125,7 @@ export declare function createMcpFormSession(): {
115
125
  readonly expiresAt: number;
116
126
  readonly updatedAt: number;
117
127
  readonly revision: number;
128
+ readonly denialNote?: string;
118
129
  readonly evidence?: readonly {
119
130
  readonly kind: "artifact";
120
131
  readonly ref: string;
@@ -126,13 +137,20 @@ export declare function createMcpFormSession(): {
126
137
  readonly canSetAutoApprove: boolean;
127
138
  };
128
139
  readonly review?: {
140
+ readonly kind: "exec" | "extension-install";
141
+ readonly form: "direct" | "shell";
129
142
  readonly command: string;
130
143
  readonly cwd: string;
131
144
  readonly justification: string;
145
+ readonly argv?: readonly string[];
132
146
  readonly prefix?: readonly string[];
133
147
  readonly warning?: string;
134
148
  readonly alwaysAllowCoverage?: string;
135
149
  readonly alwaysAllowUnavailableReason?: string;
150
+ readonly risk?: {
151
+ readonly summary: string;
152
+ readonly detail: string;
153
+ };
136
154
  };
137
155
  }[];
138
156
  }, {
@@ -169,6 +187,7 @@ export declare function createMcpFormSession(): {
169
187
  readonly expiresAt: number;
170
188
  readonly updatedAt: number;
171
189
  readonly revision: number;
190
+ readonly denialNote?: string;
172
191
  readonly evidence?: readonly {
173
192
  readonly kind: "artifact";
174
193
  readonly ref: string;
@@ -180,13 +199,20 @@ export declare function createMcpFormSession(): {
180
199
  readonly canSetAutoApprove: boolean;
181
200
  };
182
201
  readonly review?: {
202
+ readonly kind: "exec" | "extension-install";
203
+ readonly form: "direct" | "shell";
183
204
  readonly command: string;
184
205
  readonly cwd: string;
185
206
  readonly justification: string;
207
+ readonly argv?: readonly string[];
186
208
  readonly prefix?: readonly string[];
187
209
  readonly warning?: string;
188
210
  readonly alwaysAllowCoverage?: string;
189
211
  readonly alwaysAllowUnavailableReason?: string;
212
+ readonly risk?: {
213
+ readonly summary: string;
214
+ readonly detail: string;
215
+ };
190
216
  };
191
217
  }[];
192
218
  }>;
@@ -49,15 +49,18 @@ export function createMcpFormSession() {
49
49
  if (input.decision === "always-allow" &&
50
50
  entry.alwaysAllow === undefined)
51
51
  return { kind: "refused", reason: "not-pending" };
52
+ if (input.denialNote !== undefined && input.decision !== "deny")
53
+ return { kind: "refused", reason: "not-pending" };
52
54
  if (input.decision === "always-allow")
53
55
  entry.alwaysAllow?.("prefix");
54
- const result = await entry.approvals.answer({
56
+ const answer = {
55
57
  recordId: entry.approval.record.recordId,
56
- decision: input.decision === "always-allow"
57
- ? "approve"
58
- : input.decision,
58
+ decision: input.decision === "deny" ? "deny" : "approve",
59
59
  expectedRevision: input.expectedRevision,
60
- });
60
+ };
61
+ const result = await entry.approvals.answer(input.denialNote === undefined
62
+ ? answer
63
+ : { ...answer, denialNote: input.denialNote });
61
64
  return result;
62
65
  }
63
66
  finally {
@@ -1,5 +1,5 @@
1
1
  import type { AssistantMessage } from "@earendil-works/pi-ai";
2
- import type { ProviderRejection } from "@fungi.computer/mule";
2
+ import { type ProviderRejection } from "@fungi.computer/mule";
3
3
  /** Evidence owned by one Local request, never inferred from assistant prose. */
4
4
  export type ModelFailureInput = Readonly<{
5
5
  refusal?: Readonly<{
@@ -1,24 +1,44 @@
1
1
  import { isContextOverflow } from "@earendil-works/pi-ai/utils/overflow";
2
+ import { isRetryableProviderRejection, } from "@fungi.computer/mule";
2
3
  /** Local retries only a typed transient, never cancellation or a turn refusal. */
3
4
  export function modelFailureRetryable(failure) {
4
5
  return (failure.refusal === undefined &&
5
6
  failure.upstream?.stopReason !== "aborted" &&
6
- failure.rejection?.code === "provider_unavailable");
7
+ isRetryableProviderRejection(failure.rejection));
7
8
  }
8
9
  /** The one owned wording for a failed Local model request. */
9
10
  export function modelFailureMessage(provider, failure) {
11
+ const name = provider === "anthropic"
12
+ ? "Anthropic"
13
+ : provider === "openai"
14
+ ? "OpenAI"
15
+ : provider;
10
16
  if (failure.refusal !== undefined)
11
17
  return `${failure.refusal.category}: ${failure.refusal.message}`;
12
18
  // Typed subscription evidence wins over all SDK/provider wording.
13
- if (failure.rejection?.code === "usage-limit-reached")
14
- return `${provider} usage limit reached (usage-limit-reached). Check ChatGPT Settings > Usage, then try again later.`;
15
- if (modelFailureRetryable(failure))
16
- return `${provider} service unavailable. Try again shortly.`;
19
+ if (failure.rejection?.code === "usage-limit-reached") {
20
+ const subscription = provider === "anthropic"
21
+ ? " on your Claude subscription"
22
+ : provider === "openai"
23
+ ? " on your ChatGPT subscription"
24
+ : "";
25
+ const resetAt = failure.rejection.resetAt;
26
+ const reset = resetAt === undefined
27
+ ? ""
28
+ : `; resets at ${new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(resetAt)}`;
29
+ return `${name} usage limit reached${subscription}${reset}.`;
30
+ }
31
+ if (failure.rejection?.code === "rate-limit")
32
+ return `${name} rate limit reached; try again shortly.`;
33
+ if (failure.rejection?.code === "overloaded")
34
+ return `${name} is overloaded; try again shortly.`;
35
+ if (failure.rejection?.code === "provider_unavailable")
36
+ return `${name} is unavailable; try again shortly.`;
17
37
  if (failure.upstream !== undefined) {
18
38
  if (isContextOverflow(failure.upstream))
19
- return `${provider} request exceeds the context window. Compact the Session or choose another model.`;
39
+ return `${name} request exceeds the context window; compact the Session or choose another model.`;
20
40
  }
21
41
  if (failure.rejection?.code === "provider_rejected")
22
- return `${provider} rejected the model request (${failure.rejection.code}, HTTP ${String(failure.rejection.status)}). Check /login or choose another model with /model.`;
23
- return `${provider} model request failed without a provider rejection. Retry the request.`;
42
+ return `${name} rejected the request (HTTP ${String(failure.rejection.status)}); check your sign-in or choose another model.`;
43
+ return `${name} model request failed; try again.`;
24
44
  }
@@ -18,13 +18,19 @@ export type PrivateBunSqliteDatabase = {
18
18
  };
19
19
  /** The actual Node connection or Local's explicit native Bun adapter. */
20
20
  export type PrivateSqliteDatabase = DatabaseSync | PrivateBunSqliteDatabase;
21
+ /**
22
+ * How long a store's writer waits out a reader's shared lock before its
23
+ * commit fails. Status reads and the Board read a live store; a reader holds
24
+ * its lock for one statement, so this bounds a rare wait, never a hang.
25
+ */
26
+ export declare const storeWriteBusyTimeoutMs = 2000;
21
27
  /**
22
28
  * A listing owns this database until its existing finally close. Bun's
23
29
  * node:sqlite prepare/close leaves native statements alive until GC. Native
24
30
  * close(true) on the native Bun driver explicitly finalizes every statement
25
31
  * before releasing the connection, including when a read or decoding fails.
26
32
  */
27
- export declare function openPrivateReadOnlySqliteDatabase(path: string): PrivateSqliteDatabase;
33
+ export declare function openPrivateReadOnlySqliteDatabase(path: string, timeout?: number): PrivateSqliteDatabase;
28
34
  /** Writable connections own their statements through deterministic close. */
29
35
  export declare function openPrivateWritableSqliteDatabase(path: string, timeout: number): PrivateSqliteDatabase;
30
36
  export type PrivateSqliteFailureCode = "busy" | "locked" | "corrupt" | "not_a_database" | "other";
@@ -7,14 +7,26 @@ const decodeSqliteRow = Schema.decodeUnknownSync(sqliteRowSchema);
7
7
  const bunSqlite = Object.hasOwn(process.versions, "bun")
8
8
  ? await import("bun:sqlite")
9
9
  : undefined;
10
+ /**
11
+ * How long a reader waits out a live runtime's commit. Stores use a rollback
12
+ * journal, so a commit briefly locks readers out; a reader that gives up at
13
+ * once would read a busy store as an unreadable one.
14
+ */
15
+ const readBusyTimeoutMs = 250;
16
+ /**
17
+ * How long a store's writer waits out a reader's shared lock before its
18
+ * commit fails. Status reads and the Board read a live store; a reader holds
19
+ * its lock for one statement, so this bounds a rare wait, never a hang.
20
+ */
21
+ export const storeWriteBusyTimeoutMs = 2_000;
10
22
  /**
11
23
  * A listing owns this database until its existing finally close. Bun's
12
24
  * node:sqlite prepare/close leaves native statements alive until GC. Native
13
25
  * close(true) on the native Bun driver explicitly finalizes every statement
14
26
  * before releasing the connection, including when a read or decoding fails.
15
27
  */
16
- export function openPrivateReadOnlySqliteDatabase(path) {
17
- return openPrivateSqliteDatabase(path, true, 0);
28
+ export function openPrivateReadOnlySqliteDatabase(path, timeout = readBusyTimeoutMs) {
29
+ return openPrivateSqliteDatabase(path, true, timeout);
18
30
  }
19
31
  /** Writable connections own their statements through deterministic close. */
20
32
  export function openPrivateWritableSqliteDatabase(path, timeout) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/truffle",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "Truffle: Fungi's local Agent runtime, bundled into the fungi command",
6
6
  "type": "module",
@@ -50,9 +50,21 @@
50
50
  "types": "./dist/local-extensions.d.ts",
51
51
  "import": "./dist/local-extensions.js"
52
52
  },
53
+ "./upgrade": {
54
+ "types": "./dist/local-upgrade.d.ts",
55
+ "import": "./dist/local-upgrade.js"
56
+ },
53
57
  "./rules": {
54
58
  "types": "./dist/private-local-exec-rules.d.ts",
55
59
  "import": "./dist/private-local-exec-rules.js"
60
+ },
61
+ "./board-agents": {
62
+ "types": "./dist/local-board-agents.d.ts",
63
+ "import": "./dist/local-board-agents.js"
64
+ },
65
+ "./board-shipping": {
66
+ "types": "./dist/local-board-shipping.d.ts",
67
+ "import": "./dist/local-board-shipping.js"
56
68
  }
57
69
  },
58
70
  "files": [
@@ -65,26 +77,26 @@
65
77
  "@cf-wasm/photon": "0.4.0",
66
78
  "@earendil-works/pi-ai": "1.3.0",
67
79
  "@effect/platform-node": "4.0.2",
68
- "@fungi.computer/agar": "0.3.1",
69
- "@fungi.computer/approvals": "0.1.0",
70
- "@fungi.computer/bird-dog": "0.2.0",
80
+ "@fungi.computer/agar": "0.5.0",
81
+ "@fungi.computer/approvals": "0.2.0",
82
+ "@fungi.computer/bird-dog": "0.3.0",
71
83
  "@fungi.computer/cairn": "0.1.0",
72
84
  "@fungi.computer/capnweb": "0.12.0",
73
85
  "@fungi.computer/carrier": "0.2.0",
74
86
  "@fungi.computer/cordyceps": "0.4.0",
75
87
  "@fungi.computer/gills": "0.2.0",
76
- "@fungi.computer/graft": "0.1.2",
77
- "@fungi.computer/knapsack": "0.3.0",
78
- "@fungi.computer/lantern": "0.3.0",
79
- "@fungi.computer/mycelium": "0.2.2",
80
- "@fungi.computer/nacre": "0.0.2",
88
+ "@fungi.computer/graft": "0.1.4",
89
+ "@fungi.computer/knapsack": "0.4.0",
90
+ "@fungi.computer/lantern": "0.3.1",
91
+ "@fungi.computer/mycelium": "0.2.3",
92
+ "@fungi.computer/nacre": "0.1.0",
81
93
  "@fungi.computer/seam": "0.2.0",
82
- "@fungi.computer/shiitake": "0.4.0",
83
- "@fungi.computer/shiitake-extensions": "0.2.0",
84
- "@fungi.computer/veil": "0.2.0",
94
+ "@fungi.computer/shiitake": "0.6.0",
95
+ "@fungi.computer/shiitake-extensions": "0.4.0",
96
+ "@fungi.computer/veil": "0.2.1",
85
97
  "@fungi.computer/watchdog": "0.1.0",
86
- "@fungi.computer/whistle": "0.1.0",
87
- "@fungi.computer/zookeeper": "0.0.3",
98
+ "@fungi.computer/whistle": "0.3.0",
99
+ "@fungi.computer/zookeeper": "0.0.5",
88
100
  "@jellybrick/dbus-next": "0.11.4",
89
101
  "@modelcontextprotocol/client": "2.0.0",
90
102
  "@napi-rs/keyring": "2.1.2",
@@ -97,16 +109,19 @@
97
109
  "miniflare": "5.20260828.0-alpha",
98
110
  "proper-lockfile": "4.1.2",
99
111
  "re2js": "1.3.3",
112
+ "semver": "7.8.5",
113
+ "shell-quote": "1.10.0",
100
114
  "write-file-atomic": "8.0.0",
101
115
  "yaml": "2.9.0",
102
116
  "zod": "4.4.3"
103
117
  },
104
118
  "devDependencies": {
105
119
  "@cloudflare/codemode": "0.5.1",
106
- "@fungi.computer/mule": "0.2.0",
120
+ "@fungi.computer/mule": "0.2.1",
107
121
  "@modelcontextprotocol/server": "2.0.0",
108
122
  "@types/node": "24.13.3",
109
123
  "@types/proper-lockfile": "4.1.4",
124
+ "@types/semver": "7.8.0",
110
125
  "@types/write-file-atomic": "4.0.3",
111
126
  "bun": "1.4.2",
112
127
  "bun-types": "1.4.1",
@@ -149,12 +164,13 @@
149
164
  "build": "if [ ! -f src/private-local-native-digests.js ]; then pnpm run build:native; fi && node scripts/generate-native-digests.mjs --verify && pnpm run clean && tsc -p tsconfig.build.json && cp src/private-local-native-digests.js dist/private-local-native-digests.js && node scripts/copy-seatbelt.mjs && esbuild src/internal/private-workerd-sandbox-worker.mjs --outfile=dist/internal/private-workerd-sandbox-worker.mjs --bundle --format=esm --platform=neutral --conditions=workerd,browser --target=es2024 --external:cloudflare:workers --metafile=dist/internal/sandbox-metafile.json --log-level=warning && esbuild src/internal/local-image-fit-worker.mjs --outfile=dist/internal/local-image-fit-worker.mjs --bundle --format=esm --platform=node --target=es2024 --metafile=dist/internal/image-fit-metafile.json --log-level=warning",
150
165
  "clean": "node --input-type=module --eval \"import { rm } from 'node:fs/promises'; await rm('dist', { recursive: true, force: true });\"",
151
166
  "test": "pnpm run test:node && pnpm run test:merge && pnpm run test:stop-all",
167
+ "soak:memory": "bun scripts/service-memory-soak.mjs",
152
168
  "test:stop-all": "bun test ./test/private-local-stop-all.bun.test.ts",
153
169
  "test:full": "pnpm run test && pnpm run test:bun",
154
170
  "test:node": "../../node_modules/.bin/vitest run",
155
171
  "test:bun": "pnpm run test:mesh-link && pnpm run test:bun:batch && bun test ./test/private-workerd-deadline.bun.test.ts",
156
172
  "test:mesh-link": "bun test ./test/private-local-mesh-links.bun.test.ts",
157
- "test:bun:batch": "bun test ./test/private-sqlite-approval-driver.bun.test.ts ./test/mcp-form-service.bun.test.ts ./test/private-local-service-discovery.bun.test.ts ./test/private-local-service-listener.bun.test.ts ./test/private-local-service-manager.bun.test.ts ./test/private-local-service-rebind.bun.test.ts ./test/private-local-service-stop.bun.test.ts ./test/private-local-service-ensure.bun.test.ts ./test/private-local-service-identity.bun.test.ts ./test/private-local-service-kill-resume.bun.test.ts ./test/private-local-service-idle-owners.bun.test.ts ./test/private-local-service-idle-delegation.bun.test.ts ./test/private-local-mail-retry.bun.test.ts ./test/private-local-service-log-native.bun.test.ts ./test/private-local-service-log.bun.test.ts ./test/private-local-sqlite-lifetime.bun.test.ts ./test/private-sqlite-driver.bun.test.ts ./test/private-local-reload.bun.test.ts ./test/private-local-auth-rpc.bun.test.ts ./test/private-workerd-forage.bun.test.ts ./test/local-extensions.bun.test.ts ./test/local-zookeeper-slot.bun.test.ts ./test/local-extension-skills.bun.test.ts ./test/private-local-sandbox-service.bun.test.ts ./test/private-local-native-service.bun.test.ts",
173
+ "test:bun:batch": "bun test ./test/private-sqlite-approval-driver.bun.test.ts ./test/mcp-form-service.bun.test.ts ./test/private-local-service-discovery.bun.test.ts ./test/private-local-service-listener.bun.test.ts ./test/private-local-service-manager.bun.test.ts ./test/private-local-service-rebind.bun.test.ts ./test/private-local-service-stop.bun.test.ts ./test/private-local-service-ensure.bun.test.ts ./test/private-local-service-identity.bun.test.ts ./test/private-local-service-kill-resume.bun.test.ts ./test/private-local-service-idle-owners.bun.test.ts ./test/private-local-service-idle-delegation.bun.test.ts ./test/private-local-mail-retry.bun.test.ts ./test/private-local-service-log-native.bun.test.ts ./test/private-local-service-log.bun.test.ts ./test/private-local-sqlite-lifetime.bun.test.ts ./test/private-sqlite-driver.bun.test.ts ./test/private-local-reload.bun.test.ts ./test/private-local-auth-rpc.bun.test.ts ./test/private-workerd-forage.bun.test.ts ./test/local-extensions.bun.test.ts ./test/local-extension-archive.bun.test.ts ./test/local-zookeeper-slot.bun.test.ts ./test/local-extension-skills.bun.test.ts ./test/private-local-sandbox-service.bun.test.ts ./test/private-local-native-service.bun.test.ts",
158
174
  "build:native": "fungi-apply-patch-build --out native/bin && node scripts/build-bwrap.mjs --out native/bin && node scripts/generate-native-digests.mjs",
159
175
  "test:patch": "node --test --test-timeout=30000 test/apply-patch.native.mjs",
160
176
  "test:sandbox:linux": "node --import tsx test/fixtures/local-sandbox-proof.mts",