openmausbot 0.1.69 → 0.1.71
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/assets/{index-BmHupgVJ.js → index-CJtyBonr.js} +1 -1
- package/dist/assets/index-CYLdYtYp.css +1 -0
- package/dist/assets/index-Di4ovwu5.js +289 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +6 -2
- package/dist-server/drivers/agents-proxy.js +305 -16
- package/dist-server/drivers/pi-mcp-extension.ts +1 -1
- package/dist-server/index.js +3141 -1124
- package/dist-server/local-computer-proxy.js +2 -0
- package/dist-server/local-computer.js +2 -1
- package/dist-server/mcp-gate.js +543 -0
- package/dist-server/mcp-server.js +38 -0
- package/dist-server/openmausbot.js +772 -260
- package/dist-server/pair-cli.js +772 -260
- package/dist-server/prepare-cloudflared.js +20 -3
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/agent-tool-policy.js +29 -0
- package/dist-server/server/bot-package.js +55 -2
- package/dist-server/server/browser-live.js +12 -7
- package/dist-server/server/config.js +40 -4
- package/dist-server/server/delegations.js +132 -74
- package/dist-server/server/drivers/acp/core.js +2 -0
- package/dist-server/server/drivers/acp/cursor.js +1 -1
- package/dist-server/server/drivers/acp/grok.js +5 -1
- package/dist-server/server/drivers/agents-proxy.js +324 -16
- package/dist-server/server/drivers/boxagent.js +1 -1
- package/dist-server/server/drivers/claude-login-auth.js +102 -9
- package/dist-server/server/drivers/claude.js +298 -21
- package/dist-server/server/drivers/codex.js +13 -4
- package/dist-server/server/drivers/pi.js +1 -1
- package/dist-server/server/engine-install.js +137 -0
- package/dist-server/server/env-path.js +12 -0
- package/dist-server/server/harness/bus.js +1 -1
- package/dist-server/server/harness/registry.js +31 -6
- package/dist-server/server/index.js +860 -215
- package/dist-server/server/local-computer.js +1 -1
- package/dist-server/server/mcp-gate-config.js +56 -0
- package/dist-server/server/mcp-gate.js +227 -0
- package/dist-server/server/mcp-trim.js +182 -0
- package/dist-server/server/memory-journal.js +440 -0
- package/dist-server/server/memory-store.js +308 -0
- package/dist-server/server/message-db.js +90 -3
- package/dist-server/server/package-export.js +25 -0
- package/dist-server/server/peer-approval.js +1 -1
- package/dist-server/server/peer-provenance.js +6 -2
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/redact.js +15 -0
- package/dist-server/server/resume-recovery.js +46 -0
- package/dist-server/server/routines.js +116 -55
- package/dist-server/server/screen-frame-gate.js +9 -6
- package/dist-server/server/skill-library.js +5 -5
- package/dist-server/server/steer-queue.js +20 -1
- package/dist-server/server/store.js +29 -7
- package/dist-server/server/system-prompt.js +14 -1
- package/dist-server/server/team-backup.js +53 -4
- package/dist-server/server/tool-summary.js +39 -0
- package/dist-server/server/tts/speech-text.js +3 -1
- package/dist-server/server/turn-context.js +18 -0
- package/dist-server/server/workspace.js +321 -22
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/team-backup.js +14 -2
- package/dist-server/vps-container-mcp.js +6 -2
- package/package.json +1 -1
- package/skills/create-verification-skill/SKILL.md +2 -3
- package/dist/assets/index-COp5lMnv.js +0 -279
- package/dist/assets/index-Jk7TIeyW.css +0 -1
|
@@ -173,9 +173,25 @@ async function releaseBytes(asset) {
|
|
|
173
173
|
const cached = cacheDirectory ? join(cacheDirectory, asset.name) : "";
|
|
174
174
|
if (cached && existsSync(cached)) return readFileSync(cached);
|
|
175
175
|
const url = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${asset.name}`;
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
const retryableStatuses = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
177
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
178
|
+
let failure;
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(12e4) });
|
|
181
|
+
if (response.ok) return Buffer.from(await response.arrayBuffer());
|
|
182
|
+
await response.body?.cancel().catch(() => {
|
|
183
|
+
});
|
|
184
|
+
failure = new Error(`could not download ${asset.name}: HTTP ${response.status}`);
|
|
185
|
+
if (!retryableStatuses.has(response.status)) throw failure;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (!(error instanceof TypeError) && !["TimeoutError", "AbortError"].includes(error?.name)) throw error;
|
|
188
|
+
failure = error;
|
|
189
|
+
}
|
|
190
|
+
if (attempt === 3) throw failure;
|
|
191
|
+
const delay = 1e3 * 2 ** (attempt - 1);
|
|
192
|
+
console.warn(`cloudflared download attempt ${attempt}/3 failed: ${failure.message}; retrying in ${delay}ms`);
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
194
|
+
}
|
|
179
195
|
}
|
|
180
196
|
async function stageTarget(root, target) {
|
|
181
197
|
const asset = CLOUDFLARED_ASSETS[target];
|
|
@@ -251,6 +267,7 @@ export {
|
|
|
251
267
|
executableTarget,
|
|
252
268
|
parsePrepareCloudflaredArgs,
|
|
253
269
|
prepareCloudflared,
|
|
270
|
+
releaseBytes,
|
|
254
271
|
sha256,
|
|
255
272
|
targetForCurrentHost,
|
|
256
273
|
targetsForHost,
|
|
@@ -17,6 +17,7 @@ var SPAWNED_PROXIES = {
|
|
|
17
17
|
agents: resolveProxy("drivers/agents-proxy"),
|
|
18
18
|
dweb: resolveProxy("drivers/dweb-proxy"),
|
|
19
19
|
connectors: resolveProxy("connector-proxy"),
|
|
20
|
+
mcpGate: resolveProxy("mcp-gate"),
|
|
20
21
|
phone: resolveProxy("drivers/phone-proxy"),
|
|
21
22
|
// Loaded by the external `pi` process via `-e`, not by this server — but
|
|
22
23
|
// resolved through the same single source of truth so the packaged layout
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Reviewed local reads exposed by the built-in agents MCP integration.
|
|
2
|
+
* This exact catalog supplies MCP tool metadata only; names, prefixes, or
|
|
3
|
+
* third-party annotations alone must never grant read access.
|
|
4
|
+
* Session recall keeps its own-bot scope and may record a deduplicated access
|
|
5
|
+
* disclosure in a room; it does not edit recalled messages or launch work.
|
|
6
|
+
*/
|
|
7
|
+
export const READ_ONLY_AGENT_TOOL_NAMES = new Set([
|
|
8
|
+
"list_bots",
|
|
9
|
+
"list_rooms",
|
|
10
|
+
"list_threads",
|
|
11
|
+
"check_delegation",
|
|
12
|
+
"wait_delegation",
|
|
13
|
+
"session_search",
|
|
14
|
+
"session_read",
|
|
15
|
+
"list_routines",
|
|
16
|
+
"skills_list",
|
|
17
|
+
]);
|
|
18
|
+
const READ_ONLY_ANNOTATIONS = Object.freeze({
|
|
19
|
+
readOnlyHint: true,
|
|
20
|
+
destructiveHint: false,
|
|
21
|
+
idempotentHint: true,
|
|
22
|
+
openWorldHint: false,
|
|
23
|
+
});
|
|
24
|
+
export function isReadOnlyAgentTool(name) {
|
|
25
|
+
return READ_ONLY_AGENT_TOOL_NAMES.has(name);
|
|
26
|
+
}
|
|
27
|
+
export function agentToolAnnotations(name) {
|
|
28
|
+
return isReadOnlyAgentTool(name) ? READ_ONLY_ANNOTATIONS : undefined;
|
|
29
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
3
3
|
import { schemaIssue } from "./schema.js";
|
|
4
|
+
import { isSkillName, parseSkillMd, SKILL_FILE_MAX_BYTES } from "./skills.js";
|
|
4
5
|
import { BOT_PROFILE_LIMITS } from "../shared/bot-profile.js";
|
|
5
6
|
export const BOT_PACKAGE_FORMAT = "openmaus.package";
|
|
6
7
|
export const BOT_PACKAGE_VERSION = 1;
|
|
7
8
|
export const BOTMRR_MARKDOWN_VERSION = 1;
|
|
9
|
+
export const BOT_PACKAGE_SKILLS_VERSION = 1;
|
|
10
|
+
export const BOT_PACKAGE_MAX_SKILLS = 20;
|
|
11
|
+
const BOT_PACKAGE_MARKDOWN_MAX_BYTES = 1_000_000;
|
|
8
12
|
const COLORS = [
|
|
9
13
|
"green",
|
|
10
14
|
"blue",
|
|
@@ -27,6 +31,24 @@ const key = requiredText(64).regex(/^[a-z0-9][a-z0-9_-]*$/, {
|
|
|
27
31
|
message: "may only contain lowercase letters, numbers, - and _",
|
|
28
32
|
});
|
|
29
33
|
const MAX_DATE_MS = 8_640_000_000_000_000;
|
|
34
|
+
const skillName = requiredText(64).refine(isSkillName, {
|
|
35
|
+
message: "must be a lowercase skill name",
|
|
36
|
+
});
|
|
37
|
+
const portableSource = optionalText(2_000).refine((value) => value === undefined || !/^(?:[a-z]:[\\/]|[\\/]|\\\\|file:)/i.test(value), { message: "must not be an absolute path" });
|
|
38
|
+
const skillInstructions = z
|
|
39
|
+
.string({ error: "must be text" })
|
|
40
|
+
.min(1, { message: "is required" })
|
|
41
|
+
.max(SKILL_FILE_MAX_BYTES, { message: "is too long" })
|
|
42
|
+
.refine((value) => Buffer.byteLength(value, "utf8") <= SKILL_FILE_MAX_BYTES, { message: "is too large" })
|
|
43
|
+
.refine((value) => /^---\r?\n/.test(value), { message: "must start with SKILL.md frontmatter" });
|
|
44
|
+
const portableSkillSchema = z.object({
|
|
45
|
+
name: skillName,
|
|
46
|
+
description: requiredText(1_024),
|
|
47
|
+
source: portableSource,
|
|
48
|
+
license: optionalText(200),
|
|
49
|
+
compatibility: optionalText(200),
|
|
50
|
+
instructions: skillInstructions,
|
|
51
|
+
});
|
|
30
52
|
const CLOCK_TIME = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
31
53
|
const intervalWeekdays = z.array(z.number().int().min(0).max(6)).min(1).max(7).refine((weekdays) => new Set(weekdays).size === weekdays.length, "must contain unique weekdays");
|
|
32
54
|
const intervalWindow = z.object({
|
|
@@ -113,6 +135,7 @@ const packageSchema = z.object({
|
|
|
113
135
|
mascotBody: optionalText(40),
|
|
114
136
|
}),
|
|
115
137
|
playbooks: z.array(key).max(40).optional(),
|
|
138
|
+
skills: z.array(skillName).max(BOT_PACKAGE_MAX_SKILLS).optional(),
|
|
116
139
|
})).min(1).max(200),
|
|
117
140
|
chiefOfStaff: key.optional(),
|
|
118
141
|
rooms: z.array(z.object({
|
|
@@ -144,6 +167,10 @@ const packageSchema = z.object({
|
|
|
144
167
|
triggers: z.array(requiredText(100)).min(1).max(30),
|
|
145
168
|
instructions: requiredText(24_000),
|
|
146
169
|
})).max(80).optional(),
|
|
170
|
+
skills: z.object({
|
|
171
|
+
version: z.literal(BOT_PACKAGE_SKILLS_VERSION),
|
|
172
|
+
entries: z.array(portableSkillSchema).min(1).max(BOT_PACKAGE_MAX_SKILLS),
|
|
173
|
+
}).optional(),
|
|
147
174
|
examples: z.array(z.object({
|
|
148
175
|
title: requiredText(120),
|
|
149
176
|
input: requiredText(4_000),
|
|
@@ -158,7 +185,7 @@ export function isBotPackage(value) {
|
|
|
158
185
|
value.format === BOT_PACKAGE_FORMAT;
|
|
159
186
|
}
|
|
160
187
|
function markdownDocument(markdown) {
|
|
161
|
-
if (Buffer.byteLength(markdown) >
|
|
188
|
+
if (Buffer.byteLength(markdown) > BOT_PACKAGE_MARKDOWN_MAX_BYTES)
|
|
162
189
|
throw new Error("The bot playbook is too large");
|
|
163
190
|
const frontmatter = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
164
191
|
if (!frontmatter)
|
|
@@ -206,6 +233,7 @@ export function parseBotPackage(value) {
|
|
|
206
233
|
};
|
|
207
234
|
const agents = unique(pkg.agents.map((agent) => agent.key), "agent");
|
|
208
235
|
const playbooks = unique((pkg.playbooks ?? []).map((playbook) => playbook.key), "playbook");
|
|
236
|
+
const skills = unique((pkg.skills?.entries ?? []).map((skill) => skill.name), "skill");
|
|
209
237
|
unique((pkg.rooms ?? []).map((room) => room.key), "room");
|
|
210
238
|
unique((pkg.routines ?? []).map((routine) => routine.key), "routine");
|
|
211
239
|
if (pkg.chiefOfStaff && !agents.has(pkg.chiefOfStaff)) {
|
|
@@ -216,6 +244,28 @@ export function parseBotPackage(value) {
|
|
|
216
244
|
if (!playbooks.has(playbook))
|
|
217
245
|
throw new Error(`Agent ${agent.key} references unknown playbook: ${playbook}`);
|
|
218
246
|
}
|
|
247
|
+
const assignedSkills = unique(agent.skills ?? [], `skill in agent ${agent.key}`);
|
|
248
|
+
for (const skill of assignedSkills) {
|
|
249
|
+
if (!skills.has(skill))
|
|
250
|
+
throw new Error(`Agent ${agent.key} references unknown skill: ${skill}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const referencedSkills = new Set(pkg.agents.flatMap((agent) => agent.skills ?? []));
|
|
254
|
+
for (const skill of pkg.skills?.entries ?? []) {
|
|
255
|
+
const parsed = parseSkillMd(skill.instructions);
|
|
256
|
+
if ("error" in parsed)
|
|
257
|
+
throw new Error(`Skill ${skill.name} is invalid: ${parsed.error}`);
|
|
258
|
+
if (parsed.name !== skill.name)
|
|
259
|
+
throw new Error(`Skill ${skill.name} does not match its SKILL.md name`);
|
|
260
|
+
if (parsed.description !== skill.description)
|
|
261
|
+
throw new Error(`Skill ${skill.name} does not match its description`);
|
|
262
|
+
for (const field of ["license", "compatibility"]) {
|
|
263
|
+
if (skill[field] !== undefined && skill[field] !== parsed[field]) {
|
|
264
|
+
throw new Error(`Skill ${skill.name} does not match its ${field}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (!referencedSkills.has(skill.name))
|
|
268
|
+
throw new Error(`Skill ${skill.name} is not referenced by an agent`);
|
|
219
269
|
}
|
|
220
270
|
for (const room of pkg.rooms ?? []) {
|
|
221
271
|
const members = unique(room.members, `member in room ${room.key}`);
|
|
@@ -301,7 +351,10 @@ export function renderBotPackageMarkdown(document) {
|
|
|
301
351
|
const connections = pkg.requirements.apps.length
|
|
302
352
|
? pkg.requirements.apps.map((app) => `- **${app.label}${app.optional ? " (optional)" : ""}:** ${app.reason}`).join("\n")
|
|
303
353
|
: "- No connected apps are required.";
|
|
304
|
-
|
|
354
|
+
const markdown = `---\n${frontmatter}\n---\n\n# ${pkg.name}\n\n${pkg.tagline}\n\n> **Give this file to your Chief of Staff.** It is the complete team blueprint. Any agent system can run it; OpenMausBot can also install it directly.\n\n## Activation\n\nYou are the Chief of Staff for this blueprint. Read the whole document before acting. Confirm the user's goal and any missing inputs, then create or delegate to the specialist roles below. Preserve their names, ownership, boundaries, shared-room rules, and playbooks. If your platform cannot literally spawn agents, perform the roles one at a time and keep their outputs clearly separated.\n\nNever request pasted passwords or secret keys. Use the platform's normal connection flow. Do not send messages, publish content, spend money, delete data, or enable a schedule without the user's explicit approval. All routines start paused.\n\n## Mission\n\n${pkg.summary}\n\n## Outcomes\n\n${list(pkg.outcomes)}\n\n## Connections\n\n${connections}\n\n## Team\n\n${agents}\n\n## Chief of Staff\n\nThe Chief of Staff role is \`${pkg.chiefOfStaff ?? pkg.agents[0].key}\`. This role owns delegation, synthesis, conflict resolution, and the final answer to the user.\n${rooms ? `\n## Shared rooms\n\n${rooms}\n` : ""}${routines ? `\n## Suggested routines\n\n${routines}\n` : ""}${playbooks ? `\n## Playbooks\n\n${playbooks}\n` : ""}${examples ? `\n## Example job\n\n${examples}\n` : ""}\n## Completion rule\n\nReturn one clear result to the user, distinguish evidence from inference, cite source links when the work uses external material, and state what still needs human approval or a connected app.\n`;
|
|
355
|
+
if (Buffer.byteLength(markdown) > BOT_PACKAGE_MARKDOWN_MAX_BYTES)
|
|
356
|
+
throw new Error("The bot playbook is too large");
|
|
357
|
+
return markdown;
|
|
305
358
|
}
|
|
306
359
|
export function packageAgentAsMember(agent) {
|
|
307
360
|
return {
|
|
@@ -7,6 +7,9 @@ const execute = promisify(execFile);
|
|
|
7
7
|
const MAX_FRAME = 3 * 1024 * 1024;
|
|
8
8
|
const MAX_BUFFER = 4 * 1024 * 1024;
|
|
9
9
|
const HEARTBEAT_MS = 10_000;
|
|
10
|
+
// The native press resolver supplies the virtual key codes and Enter/Tab text
|
|
11
|
+
// that its raw input_keyboard relay omits. Keep unknown keys literal.
|
|
12
|
+
const DISCRETE_KEYS = new Set(["Backspace", "Enter", "Tab", "Escape", "Delete", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
|
|
10
13
|
export class BrowserLiveError extends Error {
|
|
11
14
|
status;
|
|
12
15
|
constructor(message, status = 400) { super(message); this.name = "BrowserLiveError"; this.status = status; }
|
|
@@ -308,17 +311,20 @@ export class BrowserLive {
|
|
|
308
311
|
throw new BrowserLiveError("The browser stream is not connected.", 409);
|
|
309
312
|
let command;
|
|
310
313
|
const { type, eventType, ...fields } = message;
|
|
314
|
+
const key = String(fields.code || fields.key);
|
|
311
315
|
const shortcut = Number(fields.modifiers) > 0 && !["Alt", "Control", "Meta", "Shift"].includes(String(fields.key))
|
|
312
316
|
&& !(fields.modifiers === 8 && text(fields.key, 64) && [...fields.key].length === 1);
|
|
313
|
-
|
|
314
|
-
|
|
317
|
+
// press already released the key. Match raw held keys, not the modifiers
|
|
318
|
+
// on keyUp: they may have changed, or blur may flush releases with zero.
|
|
319
|
+
if (type === "input_keyboard" && eventType === "keyUp" && !viewer.pressedKeys.has(key))
|
|
320
|
+
return;
|
|
315
321
|
if (type === "input_mouse")
|
|
316
322
|
command = { action: "input_mouse", type: eventType, ...fields };
|
|
317
323
|
else if (eventType === "char")
|
|
318
324
|
command = { action: "keyboard", subaction: "insertText", text: fields.text };
|
|
319
|
-
else if (eventType === "keyDown" && shortcut) {
|
|
320
|
-
//
|
|
321
|
-
//
|
|
325
|
+
else if (eventType === "keyDown" && (shortcut || DISCRETE_KEYS.has(String(fields.key)))) {
|
|
326
|
+
// press resolves virtual key codes, required text and modifiers, and
|
|
327
|
+
// acknowledges the complete key-down/up pair before hand-back.
|
|
322
328
|
const modifiers = fields.modifiers;
|
|
323
329
|
const chord = [[1, "Alt"], [2, "Control"], [4, "Meta"], [8, "Shift"]];
|
|
324
330
|
command = { action: "press", key: [...chord.filter(([bit]) => modifiers & bit).map(([, key]) => key), fields.key].join("+") };
|
|
@@ -326,7 +332,6 @@ export class BrowserLive {
|
|
|
326
332
|
else
|
|
327
333
|
command = { action: "input_keyboard", type: eventType, key: fields.key,
|
|
328
334
|
...(fields.code === undefined ? {} : { code: fields.code }), ...(fields.text === undefined ? {} : { text: fields.text }) };
|
|
329
|
-
const key = String(fields.code || fields.key);
|
|
330
335
|
if (command.action === "input_keyboard" && eventType === "keyDown") {
|
|
331
336
|
if (viewer.pressedKeys.size >= 64 && !viewer.pressedKeys.has(key))
|
|
332
337
|
throw new BrowserLiveError("Too many keys are held. Restart the browser before continuing.", 409);
|
|
@@ -354,7 +359,7 @@ export class BrowserLive {
|
|
|
354
359
|
}
|
|
355
360
|
if (object(JSON.parse(Buffer.concat(chunks).toString("utf8")))?.success !== true)
|
|
356
361
|
throw new Error("input failed");
|
|
357
|
-
if (command.action === "input_keyboard" && eventType === "keyUp")
|
|
362
|
+
if (command.action === "press" || (command.action === "input_keyboard" && eventType === "keyUp"))
|
|
358
363
|
viewer.pressedKeys.delete(key);
|
|
359
364
|
if (type === "input_mouse" && eventType === "mouseReleased")
|
|
360
365
|
viewer.pressedButtons.delete(String(fields.button));
|
|
@@ -195,9 +195,13 @@ const browserProfilesSchema = z.array(browserProfileSchema).max(20).superRefine(
|
|
|
195
195
|
});
|
|
196
196
|
});
|
|
197
197
|
});
|
|
198
|
+
// Deliberately non-strict: an unknown flag (such as `skillRecorder`, the
|
|
199
|
+
// pre-rename name a stale client may still PATCH) is dropped as a no-op
|
|
200
|
+
// instead of failing the whole stored config or the request.
|
|
198
201
|
const featureConfigSchema = z.object({
|
|
199
|
-
/**
|
|
200
|
-
|
|
202
|
+
/** Bots may draft skills (skill_manage, /learn) for your review. Off unless
|
|
203
|
+
* explicitly enabled. */
|
|
204
|
+
skillAuthoring: z.boolean().optional(),
|
|
201
205
|
/** Show each tool run in the transcript. Off unless explicitly enabled. */
|
|
202
206
|
showToolCalls: z.boolean().optional(),
|
|
203
207
|
/** Experimental built-in browser. Off until explicitly enabled; each bot
|
|
@@ -378,8 +382,8 @@ export function localVmMode(cfg) {
|
|
|
378
382
|
export function localVmMaxInstances(cfg) {
|
|
379
383
|
return cfg.localVm?.maxInstances ?? DEFAULT_LOCAL_VM_MAX_INSTANCES;
|
|
380
384
|
}
|
|
381
|
-
export function
|
|
382
|
-
return cfg.features?.
|
|
385
|
+
export function skillAuthoringEnabled(cfg) {
|
|
386
|
+
return cfg.features?.skillAuthoring === true;
|
|
383
387
|
}
|
|
384
388
|
export function showToolCallsEnabled(cfg) {
|
|
385
389
|
return cfg.features?.showToolCalls === true;
|
|
@@ -407,6 +411,38 @@ export function ensureDirs() {
|
|
|
407
411
|
}
|
|
408
412
|
for (const dir of [DATA_DIR, EVENTS_DIR, NATIVE_DIR])
|
|
409
413
|
mkdirSync(dir, { recursive: true });
|
|
414
|
+
migrateLegacyFeatureFlags();
|
|
415
|
+
}
|
|
416
|
+
/** `features.skillRecorder` became `features.skillAuthoring` when the Teach a
|
|
417
|
+
* skill recorder was removed. featureConfigSchema is non-strict, so
|
|
418
|
+
* parseStoredConfig would silently drop the old key (reading the opt-in as
|
|
419
|
+
* off) while saveConfig's raw section merge would carry it on disk forever.
|
|
420
|
+
* Rewrite it once, here, before the server's single loadConfig() at boot: an
|
|
421
|
+
* explicit `skillAuthoring` already on disk wins over the legacy key, and a
|
|
422
|
+
* file without the legacy key is left untouched. */
|
|
423
|
+
function migrateLegacyFeatureFlags() {
|
|
424
|
+
const p = join(DATA_DIR, "config.json");
|
|
425
|
+
try {
|
|
426
|
+
if (!existsSync(p))
|
|
427
|
+
return;
|
|
428
|
+
const disk = jsonObjectSchema.safeParse(parseJson(readFileSync(p, "utf8")));
|
|
429
|
+
if (!disk.success)
|
|
430
|
+
return;
|
|
431
|
+
const features = jsonObjectSchema.safeParse(disk.data.features);
|
|
432
|
+
if (!features.success || !Object.hasOwn(features.data, "skillRecorder"))
|
|
433
|
+
return;
|
|
434
|
+
const next = { ...features.data };
|
|
435
|
+
if (!Object.hasOwn(next, "skillAuthoring"))
|
|
436
|
+
next.skillAuthoring = next.skillRecorder === true;
|
|
437
|
+
delete next.skillRecorder;
|
|
438
|
+
writeFileAtomic(p, JSON.stringify({ ...disk.data, features: next }, null, 2), { mode: 0o600 });
|
|
439
|
+
console.log(`[config] features.skillRecorder renamed to features.skillAuthoring (${String(next.skillAuthoring)})`);
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
// A readable but unwritable config would otherwise lose the opt-in on
|
|
443
|
+
// every boot with no trace: parseStoredConfig drops the legacy key.
|
|
444
|
+
console.error(`[config] could not rename features.skillRecorder: ${error instanceof Error ? error.message : String(error)}`);
|
|
445
|
+
}
|
|
410
446
|
}
|
|
411
447
|
export function loadConfig() {
|
|
412
448
|
let cfg = {};
|
|
@@ -86,19 +86,26 @@ export function threadsWaitingOn(toBotId) {
|
|
|
86
86
|
}
|
|
87
87
|
/** Mark a target's observed busy period as finished and return the source
|
|
88
88
|
* threads that should be retried. This makes retries count distinct busy
|
|
89
|
-
* periods, not unrelated drain requests on the same source thread.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
* periods, not unrelated drain requests on the same source thread.
|
|
90
|
+
* `only` narrows the release: a bot that is still busy in one thread has
|
|
91
|
+
* nevertheless freed a slot for the fresh-thread handoffs waiting on it,
|
|
92
|
+
* while its active-thread handoffs go on waiting for it to go idle. */
|
|
93
|
+
export function releaseDelegationsWaitingOn(toBotId, only) {
|
|
94
|
+
const released = [];
|
|
95
|
+
for (const [threadId, items] of pendingDelegations) {
|
|
96
|
+
let any = false;
|
|
97
|
+
for (const item of items) {
|
|
98
|
+
if (item.toBotId !== toBotId || item.waitingOnBusy !== true || (only && !only(item)))
|
|
99
|
+
continue;
|
|
100
|
+
delete item.waitingOnBusy;
|
|
101
|
+
any = true;
|
|
98
102
|
}
|
|
103
|
+
if (any)
|
|
104
|
+
released.push(threadId);
|
|
99
105
|
}
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
if (released.length)
|
|
107
|
+
savePending();
|
|
108
|
+
return released;
|
|
102
109
|
}
|
|
103
110
|
function savePending() {
|
|
104
111
|
try {
|
|
@@ -140,6 +147,9 @@ export function _loadPending() {
|
|
|
140
147
|
if (typeof item.originatingGroupId === "string" && item.originatingGroupId) {
|
|
141
148
|
loaded.originatingGroupId = item.originatingGroupId;
|
|
142
149
|
}
|
|
150
|
+
if (typeof item.targetThreadId === "string" && item.targetThreadId) {
|
|
151
|
+
loaded.targetThreadId = item.targetThreadId;
|
|
152
|
+
}
|
|
143
153
|
return [loaded];
|
|
144
154
|
});
|
|
145
155
|
if (items.length)
|
|
@@ -195,6 +205,7 @@ export function pendingDelegationSnapshot() {
|
|
|
195
205
|
sourceBotId: item.sourceBotId,
|
|
196
206
|
toBotId: item.toBotId,
|
|
197
207
|
...(item.reason ? { reason: item.reason } : {}),
|
|
208
|
+
...(item.targetThreadId ? { targetThreadId: item.targetThreadId } : {}),
|
|
198
209
|
})));
|
|
199
210
|
}
|
|
200
211
|
/** How many handoffs one turn may queue. Small on purpose: this is the only
|
|
@@ -231,18 +242,27 @@ export function queueDelegation(bus, from, item, maxDepth, sourceThreadId = from
|
|
|
231
242
|
list.push({ ...item, id, sourceBotId: from.id, attempts: 0, ...(groupId ? { originatingGroupId: groupId } : {}) });
|
|
232
243
|
pendingDelegations.set(sourceThreadId, list);
|
|
233
244
|
savePending();
|
|
234
|
-
const label = `Delegated to @${target.name}${item.reason ? `: ${item.reason}` : ""}`;
|
|
235
245
|
const sourceGroup = sourceThreadId ? bus.store.groupByThread(sourceThreadId) : undefined;
|
|
236
|
-
|
|
246
|
+
// A fresh-thread handoff is announced as the thread it opened, with a
|
|
247
|
+
// link to it; a classic one as the delegation it is.
|
|
248
|
+
const openedThread = item.targetThreadId ? bus.store.taskByThread(target.id, item.targetThreadId) : undefined;
|
|
249
|
+
const chip = {
|
|
237
250
|
role: "bot",
|
|
238
251
|
kind: "activity",
|
|
239
252
|
// settled at birth: queueing is the whole act. Left open, the chip
|
|
240
253
|
// would spin until the transcript is closed — the chat never patches it
|
|
241
|
-
tool: {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
254
|
+
tool: {
|
|
255
|
+
name: openedThread
|
|
256
|
+
? `Opened thread #${openedThread.title} on ${target.name}`
|
|
257
|
+
: `Delegated to @${target.name}${item.reason ? `: ${item.reason}` : ""}`,
|
|
258
|
+
ok: true,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
if (openedThread)
|
|
262
|
+
chip.threadRef = { botId: target.id, threadId: openedThread.threadId, title: openedThread.title };
|
|
263
|
+
if (sourceGroup && !sourceGroup.dm)
|
|
264
|
+
chip.from = { botId: from.id, name: from.name, color: from.color };
|
|
265
|
+
bus.store.appendMessage(sourceThreadId, chip);
|
|
246
266
|
return { result: "ok", id };
|
|
247
267
|
}
|
|
248
268
|
/** Drain queued delegations for a source thread (called on its
|
|
@@ -418,35 +438,12 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
418
438
|
if (dropIfUnreachable(bus, sender, target, sourceThreadId, item)) {
|
|
419
439
|
return "settled";
|
|
420
440
|
}
|
|
421
|
-
if (target
|
|
422
|
-
if (item.waitingOnBusy)
|
|
423
|
-
return "requeued";
|
|
424
|
-
item.attempts += 1;
|
|
425
|
-
item.waitingOnBusy = true;
|
|
426
|
-
if (item.attempts < MAX_BUSY_ATTEMPTS) {
|
|
427
|
-
savePending();
|
|
428
|
-
bus.store.appendMessage(sourceThreadId, {
|
|
429
|
-
role: "bot",
|
|
430
|
-
kind: "activity",
|
|
431
|
-
tool: { name: `Delegation to @${target.name} waiting — they're busy (retry ${item.attempts}/${MAX_BUSY_ATTEMPTS} when they finish)` },
|
|
432
|
-
});
|
|
433
|
-
return "requeued";
|
|
434
|
-
}
|
|
435
|
-
recordDelegationReceipt({
|
|
436
|
-
id: item.id,
|
|
437
|
-
sourceThreadId,
|
|
438
|
-
toBotId: target.id,
|
|
439
|
-
toBotName: target.name,
|
|
440
|
-
status: "busy_gave_up",
|
|
441
|
-
result: `@${target.name} stayed busy through ${MAX_BUSY_ATTEMPTS} retries`,
|
|
442
|
-
});
|
|
443
|
-
bus.store.appendMessage(sourceThreadId, {
|
|
444
|
-
role: "bot",
|
|
445
|
-
kind: "activity",
|
|
446
|
-
tool: { name: `Delegation to @${target.name} canceled — still busy after ${MAX_BUSY_ATTEMPTS} retries`, ok: false },
|
|
447
|
-
});
|
|
441
|
+
if (dropIfThreadGone(bus, target, sourceThreadId, item)) {
|
|
448
442
|
return "settled";
|
|
449
443
|
}
|
|
444
|
+
const held = holdWhileTargetBusy(bus, target, sourceThreadId, item);
|
|
445
|
+
if (held)
|
|
446
|
+
return held;
|
|
450
447
|
if (item.waitingOnBusy) {
|
|
451
448
|
delete item.waitingOnBusy;
|
|
452
449
|
savePending();
|
|
@@ -494,35 +491,12 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
494
491
|
if (dropIfUnreachable(bus, currentSender, current, sourceThreadId, item)) {
|
|
495
492
|
return "settled";
|
|
496
493
|
}
|
|
497
|
-
if (current
|
|
498
|
-
if (item.waitingOnBusy)
|
|
499
|
-
return "requeued";
|
|
500
|
-
item.attempts += 1;
|
|
501
|
-
item.waitingOnBusy = true;
|
|
502
|
-
if (item.attempts < MAX_BUSY_ATTEMPTS) {
|
|
503
|
-
savePending();
|
|
504
|
-
bus.store.appendMessage(sourceThreadId, {
|
|
505
|
-
role: "bot",
|
|
506
|
-
kind: "activity",
|
|
507
|
-
tool: { name: `Delegation to @${current.name} waiting — they're busy (retry ${item.attempts}/${MAX_BUSY_ATTEMPTS} when they finish)` },
|
|
508
|
-
});
|
|
509
|
-
return "requeued";
|
|
510
|
-
}
|
|
511
|
-
recordDelegationReceipt({
|
|
512
|
-
id: item.id,
|
|
513
|
-
sourceThreadId,
|
|
514
|
-
toBotId: current.id,
|
|
515
|
-
toBotName: current.name,
|
|
516
|
-
status: "busy_gave_up",
|
|
517
|
-
result: `@${current.name} stayed busy through ${MAX_BUSY_ATTEMPTS} retries`,
|
|
518
|
-
});
|
|
519
|
-
bus.store.appendMessage(sourceThreadId, {
|
|
520
|
-
role: "bot",
|
|
521
|
-
kind: "activity",
|
|
522
|
-
tool: { name: `Delegation to @${current.name} canceled — still busy after ${MAX_BUSY_ATTEMPTS} retries`, ok: false },
|
|
523
|
-
});
|
|
494
|
+
if (dropIfThreadGone(bus, current, sourceThreadId, item)) {
|
|
524
495
|
return "settled";
|
|
525
496
|
}
|
|
497
|
+
const heldAfterApproval = holdWhileTargetBusy(bus, current, sourceThreadId, item);
|
|
498
|
+
if (heldAfterApproval)
|
|
499
|
+
return heldAfterApproval;
|
|
526
500
|
sender = currentSender;
|
|
527
501
|
target = current;
|
|
528
502
|
}
|
|
@@ -533,10 +507,94 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
533
507
|
const channel = getOrCreateChannel(bus.store, sender, target, originatingGroup);
|
|
534
508
|
mirrorExchange(bus, sender, target, item.message, channel, sourceThreadId);
|
|
535
509
|
const reasonLine = item.reason ? `\n\n[Reason: ${item.reason}]` : "";
|
|
536
|
-
|
|
537
|
-
|
|
510
|
+
// A fresh thread's first line gets the shared peer-provenance note from
|
|
511
|
+
// the harness (which knows whether the opener was unattended); the
|
|
512
|
+
// classic handoff keeps the prefix it has always had.
|
|
513
|
+
const prefixed = item.targetThreadId
|
|
514
|
+
? item.message
|
|
515
|
+
: `[Delegated by @${sender.name}, another bot in this OpenMausBot workspace. Do the work and reply directly.]\n\n${item.message}${reasonLine}`;
|
|
516
|
+
await runTarget(item.toBotId, prefixed, item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId);
|
|
538
517
|
return "dispatched";
|
|
539
518
|
}
|
|
519
|
+
/** A busy target holds the handoff. What "busy" means depends on where the
|
|
520
|
+
* turn will run: a classic delegation lands in the target's active thread,
|
|
521
|
+
* so it waits for the bot to go idle, with bounded retries so a stuck peer
|
|
522
|
+
* cannot pin the handoff forever. A fresh-thread handoff needs only a free
|
|
523
|
+
* slot, and waits in line for one without a bound — it IS the line, the
|
|
524
|
+
* same one a person's message joins at that limit. Returns null when the
|
|
525
|
+
* target can take the turn now. */
|
|
526
|
+
function holdWhileTargetBusy(bus, target, sourceThreadId, item) {
|
|
527
|
+
if (item.targetThreadId) {
|
|
528
|
+
if (bus.threadSlotFree ? bus.threadSlotFree(target.id) : !target.busy)
|
|
529
|
+
return null;
|
|
530
|
+
if (item.waitingOnBusy)
|
|
531
|
+
return "requeued";
|
|
532
|
+
item.waitingOnBusy = true;
|
|
533
|
+
savePending();
|
|
534
|
+
// one chip per wait, not one per retry: the line moves on every settle
|
|
535
|
+
if (item.attempts === 0) {
|
|
536
|
+
item.attempts = 1;
|
|
537
|
+
const title = bus.store.taskByThread(target.id, item.targetThreadId)?.title ?? "thread";
|
|
538
|
+
bus.store.appendMessage(sourceThreadId, {
|
|
539
|
+
role: "bot",
|
|
540
|
+
kind: "activity",
|
|
541
|
+
tool: { name: `Thread #${title} on @${target.name} waiting for a free slot` },
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return "requeued";
|
|
545
|
+
}
|
|
546
|
+
if (!target.busy)
|
|
547
|
+
return null;
|
|
548
|
+
if (item.waitingOnBusy)
|
|
549
|
+
return "requeued";
|
|
550
|
+
item.attempts += 1;
|
|
551
|
+
item.waitingOnBusy = true;
|
|
552
|
+
if (item.attempts < MAX_BUSY_ATTEMPTS) {
|
|
553
|
+
savePending();
|
|
554
|
+
bus.store.appendMessage(sourceThreadId, {
|
|
555
|
+
role: "bot",
|
|
556
|
+
kind: "activity",
|
|
557
|
+
tool: { name: `Delegation to @${target.name} waiting — they're busy (retry ${item.attempts}/${MAX_BUSY_ATTEMPTS} when they finish)` },
|
|
558
|
+
});
|
|
559
|
+
return "requeued";
|
|
560
|
+
}
|
|
561
|
+
recordDelegationReceipt({
|
|
562
|
+
id: item.id,
|
|
563
|
+
sourceThreadId,
|
|
564
|
+
toBotId: target.id,
|
|
565
|
+
toBotName: target.name,
|
|
566
|
+
status: "busy_gave_up",
|
|
567
|
+
result: `@${target.name} stayed busy through ${MAX_BUSY_ATTEMPTS} retries`,
|
|
568
|
+
});
|
|
569
|
+
bus.store.appendMessage(sourceThreadId, {
|
|
570
|
+
role: "bot",
|
|
571
|
+
kind: "activity",
|
|
572
|
+
tool: { name: `Delegation to @${target.name} canceled — still busy after ${MAX_BUSY_ATTEMPTS} retries`, ok: false },
|
|
573
|
+
});
|
|
574
|
+
return "settled";
|
|
575
|
+
}
|
|
576
|
+
/** The thread a fresh-thread handoff was opened in may be deleted while the
|
|
577
|
+
* handoff waits. There is nowhere for the turn to run then, and running it
|
|
578
|
+
* in the target's active thread would put a bot's job in front of the
|
|
579
|
+
* person unasked. */
|
|
580
|
+
function dropIfThreadGone(bus, target, sourceThreadId, item) {
|
|
581
|
+
if (!item.targetThreadId || bus.store.taskByThread(target.id, item.targetThreadId))
|
|
582
|
+
return false;
|
|
583
|
+
recordDelegationReceipt({
|
|
584
|
+
id: item.id,
|
|
585
|
+
sourceThreadId,
|
|
586
|
+
toBotId: target.id,
|
|
587
|
+
toBotName: target.name,
|
|
588
|
+
status: "dropped",
|
|
589
|
+
result: `the thread opened on @${target.name} was deleted before it could start`,
|
|
590
|
+
});
|
|
591
|
+
bus.store.appendMessage(sourceThreadId, {
|
|
592
|
+
role: "bot",
|
|
593
|
+
kind: "activity",
|
|
594
|
+
tool: { name: `Thread on @${target.name} canceled — it was deleted before it could start`, ok: false },
|
|
595
|
+
});
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
540
598
|
/** The source thread may be a bot's own task or a shared group where the
|
|
541
599
|
* bot is a member. This is the same check the API gate uses for group turns. */
|
|
542
600
|
function sourceThreadBelongsToBot(store, botId, threadId) {
|
|
@@ -35,6 +35,7 @@ import { supportsApprovalMode } from "../../../shared/approval-mode.js";
|
|
|
35
35
|
// packaged server dir entirely. See server/proxy-paths.ts.
|
|
36
36
|
const COMPUTER_PROXY_PATH = SPAWNED_PROXIES.computer;
|
|
37
37
|
import { appendNative } from "../native.js";
|
|
38
|
+
import { commandSummary } from "../../tool-summary.js";
|
|
38
39
|
import { SPAWNED_PROXIES } from "../../proxy-paths.js";
|
|
39
40
|
const envOr = (key, fallback) => Number(process.env[key] ?? fallback);
|
|
40
41
|
const INIT_TIMEOUT = envOr("OPENMAUS_ACP_INIT_TIMEOUT_MS", 300_000);
|
|
@@ -568,6 +569,7 @@ export function createAcpDriver(support) {
|
|
|
568
569
|
itemType: "tool",
|
|
569
570
|
itemId: u.toolCallId,
|
|
570
571
|
title: String(u.rawInput?.command ?? u.title ?? "tool").slice(0, 80),
|
|
572
|
+
summary: commandSummary(u.rawInput),
|
|
571
573
|
});
|
|
572
574
|
break;
|
|
573
575
|
}
|
|
@@ -177,7 +177,7 @@ export function decodeCursorModelText(text) {
|
|
|
177
177
|
const line = raw.trim();
|
|
178
178
|
if (!line || line.startsWith("#") || /^available\s+models?\b/i.test(line) || /^models?\b/i.test(line))
|
|
179
179
|
continue;
|
|
180
|
-
const stripped = line.replace(/^[\s
|
|
180
|
+
const stripped = line.replace(/^[\s*•-]+\s*/, "");
|
|
181
181
|
const parts = stripped.split(/\s+[—–|:]\s+|\s+-\s+|\s{2,}/);
|
|
182
182
|
const id = (parts[0] ?? "").trim();
|
|
183
183
|
const rawLabel = parts.slice(1).join(" ").trim();
|
|
@@ -208,9 +208,13 @@ const support = {
|
|
|
208
208
|
// and BEFORE `stdio` (`grok agent -m slug stdio`). Putting -m first is
|
|
209
209
|
// accepted as a TUI option and then ignored, so ACP session/new keeps
|
|
210
210
|
// [models].default (grok-4.6) and oMLX never sees a request.
|
|
211
|
+
// Auto selects Grok's native classifier; if its feature gate is disabled,
|
|
212
|
+
// residual requests still ask. Never replace it with bypassPermissions.
|
|
213
|
+
// Verified: grok 1.0.3 --help and xai-org/grok-build@37949780,
|
|
214
|
+
// crates/codegen/xai-grok-pager-bin/src/main.rs:1259-1273.
|
|
211
215
|
spawnArgs: (config, turn) => [
|
|
212
216
|
"--permission-mode",
|
|
213
|
-
config.fullAuto ? "bypassPermissions" : "default",
|
|
217
|
+
config.fullAuto ? "bypassPermissions" : turn.approvalMode === "auto" ? "auto" : "default",
|
|
214
218
|
"agent",
|
|
215
219
|
...(turn.model ? ["-m", turn.model] : []),
|
|
216
220
|
// long form on purpose: `--effort` is documented as an alias, and an
|