@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.
- package/dist/local-board-agents.d.ts +46 -0
- package/dist/local-board-agents.js +138 -0
- package/dist/local-board-shipping.d.ts +129 -0
- package/dist/local-board-shipping.js +325 -0
- package/dist/local-extensions.d.ts +2 -0
- package/dist/local-extensions.js +121 -36
- package/dist/local-login.js +33 -19
- package/dist/local-upgrade.d.ts +63 -0
- package/dist/local-upgrade.js +358 -0
- package/dist/private-local-agent-control.d.ts +8 -1
- package/dist/private-local-agent-control.js +16 -2
- package/dist/private-local-auth-rpc.d.ts +5 -0
- package/dist/private-local-auth-rpc.js +46 -4
- package/dist/private-local-client-version.js +3 -1
- package/dist/private-local-delegation.d.ts +18 -0
- package/dist/private-local-delegation.js +82 -21
- package/dist/private-local-exec-review.js +45 -8
- package/dist/private-local-extension-archive-notice.d.ts +8 -0
- package/dist/private-local-extension-archive-notice.js +108 -0
- package/dist/private-local-extension-records.d.ts +25 -0
- package/dist/private-local-extension-records.js +51 -0
- package/dist/private-local-process-identity.d.ts +14 -0
- package/dist/private-local-process-identity.js +51 -0
- package/dist/private-local-runtime-host.d.ts +7 -1
- package/dist/private-local-runtime-host.js +88 -31
- package/dist/private-local-service-contract.d.ts +1 -0
- package/dist/private-local-service-discovery.js +2 -3
- package/dist/private-local-service-owner.js +80 -9
- package/dist/private-local-service-termination.js +12 -11
- package/dist/private-local-service.js +19 -1
- package/dist/private-local-session-discovery.d.ts +16 -4
- package/dist/private-local-session-discovery.js +68 -4
- package/dist/private-mcp-form-contract.d.ts +13 -0
- package/dist/private-mcp-form-contract.js +6 -1
- package/dist/private-mcp-form-session.d.ts +26 -0
- package/dist/private-mcp-form-session.js +8 -5
- package/dist/private-provider-rejection.d.ts +1 -1
- package/dist/private-provider-rejection.js +28 -8
- package/dist/private-sqlite-owner.d.ts +7 -1
- package/dist/private-sqlite-owner.js +14 -2
- 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
|
|
56
|
+
const answer = {
|
|
55
57
|
recordId: entry.approval.record.recordId,
|
|
56
|
-
decision: input.decision === "
|
|
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
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 `${
|
|
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 `${
|
|
23
|
-
return `${
|
|
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,
|
|
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
|
+
"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.
|
|
69
|
-
"@fungi.computer/approvals": "0.
|
|
70
|
-
"@fungi.computer/bird-dog": "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.
|
|
77
|
-
"@fungi.computer/knapsack": "0.
|
|
78
|
-
"@fungi.computer/lantern": "0.3.
|
|
79
|
-
"@fungi.computer/mycelium": "0.2.
|
|
80
|
-
"@fungi.computer/nacre": "0.0
|
|
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.
|
|
83
|
-
"@fungi.computer/shiitake-extensions": "0.
|
|
84
|
-
"@fungi.computer/veil": "0.2.
|
|
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.
|
|
87
|
-
"@fungi.computer/zookeeper": "0.0.
|
|
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.
|
|
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",
|