openmausbot 0.1.72 → 0.1.74
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-BRLHsQ5g.js +298 -0
- package/dist/assets/index-ByLQ7a3F.css +1 -0
- package/dist/assets/{index-Bpai86ua.js → index-CwoQdM0z.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +9 -0
- package/dist-server/drivers/agents-proxy.js +2 -2
- package/dist-server/index.js +721 -484
- package/dist-server/mcp-gate.js +58 -16
- package/dist-server/openmausbot.js +236 -80
- package/dist-server/pair-cli.js +236 -80
- package/dist-server/server/auto-approve.js +30 -206
- package/dist-server/server/channel-queue.js +20 -2
- package/dist-server/server/config.js +36 -1
- package/dist-server/server/drivers/acp/core.js +56 -3
- package/dist-server/server/drivers/acp/grok.js +3 -1
- package/dist-server/server/drivers/agents-proxy.js +2 -2
- package/dist-server/server/drivers/antigravity-acp.js +5 -4
- package/dist-server/server/drivers/antigravity.js +9 -4
- package/dist-server/server/drivers/claude.js +54 -11
- package/dist-server/server/drivers/codex.js +86 -27
- package/dist-server/server/engine-install.js +9 -5
- package/dist-server/server/index.js +210 -231
- package/dist-server/server/message-db.js +70 -3
- package/dist-server/server/procs.js +82 -23
- package/dist-server/server/routines.js +67 -8
- package/dist-server/server/steer-queue.js +27 -7
- package/dist-server/server/team-manifest.js +1 -1
- package/dist-server/server/vps-computer.js +81 -12
- package/dist-server/server/webhooks.js +4 -1
- package/dist-server/server/workspace-backup.js +24 -9
- package/dist-server/shared/approval-mode.js +11 -10
- package/dist-server/vps-container-mcp.js +9 -0
- package/package.json +1 -1
- package/dist/assets/index-BFPYw6P-.js +0 -296
- package/dist/assets/index-D1odUdTi.css +0 -1
- package/dist-server/server/auto-review.js +0 -79
|
@@ -8,11 +8,10 @@ import { extname, join } from "node:path";
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { botAvatarUrlFromStoredPath } from "../shared/bot-avatar.js";
|
|
10
10
|
import { BOT_PROFILE_LIMITS } from "../shared/bot-profile.js";
|
|
11
|
-
import { approvalModeFor, supportsApprovalMode,
|
|
11
|
+
import { approvalModeFor, supportsApprovalMode, isEmergencyApprovalDowngrade, isApprovalMode, } from "../shared/approval-mode.js";
|
|
12
12
|
import { escapeAttribute } from "../src/lib/composer-attachments.js";
|
|
13
13
|
import { CREDENTIAL_TARGETS, credentialResumeOutcome, credentialIsConfigured, isReusableCredentialRequest, isCredentialTargetId, } from "../shared/credential-request.js";
|
|
14
|
-
import { HELD_NOTE, approvalHeldNote, approvalHeldReason, approvalModeForOrigin, autoVerdict
|
|
15
|
-
import { requestReview, resolveAutoReviewMode, shouldReview } from "./auto-review.js";
|
|
14
|
+
import { HELD_NOTE, approvalHeldNote, approvalHeldReason, approvalModeForOrigin, autoVerdict } from "./auto-approve.js";
|
|
16
15
|
import { updateClaudeCli } from "./claude-update.js";
|
|
17
16
|
import { configuredAccountDirectory, assertSeparateClaudeAccount, claudeAccountInfo, createClaudeAccountSchema, instanceSettingsSchema, newClaudeAccount } from "./claude-accounts.js";
|
|
18
17
|
import { BrowserCleanupCoordinator, finalizeBrowserCleanupMutation, requireBrowserCleanupAcknowledged, } from "./browser-lifecycle-cleanup.js";
|
|
@@ -33,7 +32,7 @@ import { chiefOfStaffSystemPrompt } from "./chief-of-staff.js";
|
|
|
33
32
|
import { peerAllowed, peerName, peerRosterSystemPrompt, reachablePeers, roomPeerRosterSystemPrompt, roomRosterLine } from "./peer-roster.js";
|
|
34
33
|
import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
|
|
35
34
|
import { containerComputerAction, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, containerRuntimeStatus, localVmRecreatableOnDemand, perBotLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, } from "./container-computer.js";
|
|
36
|
-
import { ensureDirs, instanceConfigs, loadConfig, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, maxConcurrentBotThreads, saveConfig, showToolCallsEnabled, skillAuthoringEnabled, builtInBrowserEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, } from "./config.js";
|
|
35
|
+
import { ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, maxConcurrentBotThreads, saveConfig, showToolCallsEnabled, skillAuthoringEnabled, builtInBrowserEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, } from "./config.js";
|
|
37
36
|
import { ComputerControl } from "./computer-control.js";
|
|
38
37
|
import { MAX_REMOTE_COMMAND_LENGTH } from "./remote-computer.js";
|
|
39
38
|
import { augmentedPath, findCliCandidates, resetPathCache } from "./env-path.js";
|
|
@@ -53,15 +52,15 @@ import { probeMcpServer } from "./mcp-probe.js";
|
|
|
53
52
|
import { GROUP_GOAL_MAX_TURNS, groupGoalAssignmentKey, groupGoalCompletionTurnId, groupGoalCoordinatorInstructions, groupGoalWorkerInstructions, parseGroupGoalDecision, resolveGroupGoalMember, selectGroupGoalCoordinator, } from "./group-goal-run.js";
|
|
54
53
|
import { BUILT_IN_DRIVERS } from "./drivers/builtIn.js";
|
|
55
54
|
import { getOrCreateChannel, mirrorActivity, mirrorExchange, mirrorReply } from "./comms-visibility.js";
|
|
56
|
-
import { readMessageText, recallMessages, searchMessages, closeMessageDb } from "./message-db.js";
|
|
55
|
+
import { readMessageText, recallMessages, searchMessages, closeMessageDb, chatFollowups, cancelledChatFollowup, settleChatFollowups } from "./message-db.js";
|
|
57
56
|
import { claimRecallCrossings, recallCrossingLabel } from "./recall-disclosure.js";
|
|
58
57
|
/** A session_read answer competes with the transcript for the context
|
|
59
58
|
* window; a computer-use turn's output can run to hundreds of KB. */
|
|
60
59
|
const SESSION_READ_MAX_CHARS = 8_000;
|
|
61
60
|
import { promptWithReply, transcriptText } from "./replies.js";
|
|
62
61
|
import { _loadPending, buildDelegationFailurePrompt, buildDelegationRevivalPrompt, DelegationWakeBudget, discardDelegations, drainDelegations, findDelegationReceipt, pendingDelegationInfo, pendingDelegationSnapshot, pendingThreads, queueDelegation, recordDelegationReceipt, releaseDelegationsWaitingOn, summarizeDelegatedActivity } from "./delegations.js";
|
|
63
|
-
import { cancelSteeredMessage, drainSteeredMessages, onSteeredQueueChange, queuedSteerSnapshot, queuedSteeredMessage, queuedThreadPosition, queueSteeredMessage, } from "./steer-queue.js";
|
|
64
|
-
import { cancelChannelMessage, drainChannelMessages, queuedChannelMessage, queueChannelMessage, } from "./channel-queue.js";
|
|
62
|
+
import { cancelSteeredMessage, drainSteeredMessages, onSteeredQueueChange, queuedSteerSnapshot, queuedSteeredMessage, queuedThreadPosition, queueSteeredMessage, restoreSteeredMessages, } from "./steer-queue.js";
|
|
63
|
+
import { cancelChannelMessage, drainChannelMessages, queuedChannelMessage, queueChannelMessage, restoreChannelMessages, } from "./channel-queue.js";
|
|
65
64
|
import { acceptedSendMatch, parseSendId, sendFingerprint, SendSequencer, } from "./send-idempotency.js";
|
|
66
65
|
import { EventBus } from "./harness/bus.js";
|
|
67
66
|
import { ProviderRegistry } from "./harness/registry.js";
|
|
@@ -481,6 +480,20 @@ class DirectTurnSetupCancelled extends Error {
|
|
|
481
480
|
}
|
|
482
481
|
const directTurnDispatchClaims = new Map();
|
|
483
482
|
const directTurnGenerationByThread = new Map();
|
|
483
|
+
// Stop revokes credentials before completion, but the receipt must retain its
|
|
484
|
+
// exact provider-turn owner until that completion or explicit failure cleanup.
|
|
485
|
+
const directFollowupTurns = new ProviderTurnGenerationRegistry();
|
|
486
|
+
const directFollowupSettlers = new Map();
|
|
487
|
+
function settleDirectFollowup(generation) {
|
|
488
|
+
if (!generation)
|
|
489
|
+
return;
|
|
490
|
+
const pending = directFollowupSettlers.get(generation);
|
|
491
|
+
if (!pending)
|
|
492
|
+
return;
|
|
493
|
+
directFollowupSettlers.delete(generation);
|
|
494
|
+
directFollowupTurns.deleteGeneration(pending.threadId, generation);
|
|
495
|
+
pending.settle();
|
|
496
|
+
}
|
|
484
497
|
// Keep the exact provider/profile settings that own a running conversation.
|
|
485
498
|
// Selecting another thread or changing a default must not retarget its tools.
|
|
486
499
|
const directTurnBots = new Map();
|
|
@@ -1001,6 +1014,7 @@ function checkedMemberIds(value) {
|
|
|
1001
1014
|
}
|
|
1002
1015
|
let bootSelection = { instanceId: "", model: "" };
|
|
1003
1016
|
const store = new Store(() => bootSelection);
|
|
1017
|
+
let followupsReady = false;
|
|
1004
1018
|
const sendSequencer = new SendSequencer();
|
|
1005
1019
|
bootSelection = await defaultSelection();
|
|
1006
1020
|
store.seedIfEmpty();
|
|
@@ -1184,15 +1198,11 @@ async function botOverview(bot) {
|
|
|
1184
1198
|
* this too, but no provider dispatch or later permission callback relies on
|
|
1185
1199
|
* persistence having been produced exclusively by that route. Delegation
|
|
1186
1200
|
* uses the receiving bot's grant, never the sender's — see approvalModeForOrigin. */
|
|
1187
|
-
const approvalModeForTurn = (bot, peerInitiated = false
|
|
1201
|
+
const approvalModeForTurn = (bot, peerInitiated = false) => {
|
|
1188
1202
|
const mode = approvalModeForOrigin(approvalModeFor(bot), { peerInitiated });
|
|
1189
1203
|
if (!supportsApprovalMode(registry.cliTarget(bot.modelSelection.instanceId)?.driverKind, mode)) {
|
|
1190
1204
|
return "ask";
|
|
1191
1205
|
}
|
|
1192
|
-
// Native reviewers can approve before a permission reaches this process.
|
|
1193
|
-
// Unattended Auto must therefore downgrade before spawning the provider.
|
|
1194
|
-
if (mode === "auto" && isUnattended(bot.id, threadId))
|
|
1195
|
-
return "ask";
|
|
1196
1206
|
return mode;
|
|
1197
1207
|
};
|
|
1198
1208
|
/** Privileged approval-mode transitions are deliberately absent from the
|
|
@@ -2162,7 +2172,9 @@ const askMessageByRequest = new Map(); // threadId:requestId -> messageId
|
|
|
2162
2172
|
* the engine has no asks — is fail-closed: the action was never run. The
|
|
2163
2173
|
* card is settled and a chip says so, instead of the answer vanishing into
|
|
2164
2174
|
* a 500 while the card sits open forever. */
|
|
2165
|
-
async function answerRequest(threadId, instanceId, requestId, behavior, message, decidedFor
|
|
2175
|
+
async function answerRequest(threadId, instanceId, requestId, behavior, message, decidedFor,
|
|
2176
|
+
/** "Always allow this session": the provider keeps the allow, not the app */
|
|
2177
|
+
always) {
|
|
2166
2178
|
// Snapshot the card BEFORE delivering the answer: a delivered answer
|
|
2167
2179
|
// resolves the request synchronously through the fold, which consumes
|
|
2168
2180
|
// the askMessageByRequest entry — by the time the await returns, nobody
|
|
@@ -2179,7 +2191,7 @@ async function answerRequest(threadId, instanceId, requestId, behavior, message,
|
|
|
2179
2191
|
let outcome = "unavailable";
|
|
2180
2192
|
if (instance) {
|
|
2181
2193
|
try {
|
|
2182
|
-
outcome = await instance.adapter.respondToRequest(threadId, requestId, { behavior, message });
|
|
2194
|
+
outcome = await instance.adapter.respondToRequest(threadId, requestId, { behavior, message, always: always && behavior === "allow" });
|
|
2183
2195
|
}
|
|
2184
2196
|
catch {
|
|
2185
2197
|
outcome = "unavailable";
|
|
@@ -2248,6 +2260,16 @@ function requestBehavior(value) {
|
|
|
2248
2260
|
// the last settled assistant text per thread, so a "finished" notification
|
|
2249
2261
|
// can carry what the bot actually said
|
|
2250
2262
|
const lastReply = new Map();
|
|
2263
|
+
/** the model each thread's provider session announced in session.started,
|
|
2264
|
+
* so a fallback notice can name the model Auto is unavailable for */
|
|
2265
|
+
const sessionModelByThread = new Map();
|
|
2266
|
+
/** threads already told that the provider's reviewer never started */
|
|
2267
|
+
const nativeReviewNoticed = new Set();
|
|
2268
|
+
/** a driver kind as the chat should name it: "claudeAgent" → "Claude" */
|
|
2269
|
+
const providerLabel = (provider) => {
|
|
2270
|
+
const bare = provider.replace(/Agent$/, "");
|
|
2271
|
+
return bare.charAt(0).toUpperCase() + bare.slice(1);
|
|
2272
|
+
};
|
|
2251
2273
|
/** Put a notification on the wire. Clients decide what to do with it — a
|
|
2252
2274
|
* desktop notification now, a push to a paired phone later. */
|
|
2253
2275
|
function notify(notification) {
|
|
@@ -2315,6 +2337,7 @@ const watchdog = new TurnWatchdog({
|
|
|
2315
2337
|
kind: "activity",
|
|
2316
2338
|
tool: { name: `error: no activity for ${minutes} minutes — the turn was stopped`, ok: false },
|
|
2317
2339
|
});
|
|
2340
|
+
settleDirectFollowup(stalledGeneration);
|
|
2318
2341
|
finalizeDelegationWatch(turn.threadId, false, "", "Delegated turn stalled and was stopped");
|
|
2319
2342
|
turnUsage.delete(turn.threadId);
|
|
2320
2343
|
roomStallCompletions.stall(turn.threadId);
|
|
@@ -2370,66 +2393,6 @@ const watchdog = new TurnWatchdog({
|
|
|
2370
2393
|
},
|
|
2371
2394
|
});
|
|
2372
2395
|
watchdog.start();
|
|
2373
|
-
async function reviewPermissionCard(args) {
|
|
2374
|
-
const mode = resolveAutoReviewMode(args.asker.autoReview);
|
|
2375
|
-
if (mode === "off" || !args.instance.reviewPermission)
|
|
2376
|
-
return false;
|
|
2377
|
-
const persona = [args.asker.name, args.asker.title, args.asker.description].filter(Boolean).join(" — ");
|
|
2378
|
-
const reviewed = await requestReview(args.instance.reviewPermission.bind(args.instance), {
|
|
2379
|
-
tool: args.tool,
|
|
2380
|
-
summary: args.summary,
|
|
2381
|
-
persona,
|
|
2382
|
-
});
|
|
2383
|
-
if (!reviewed)
|
|
2384
|
-
return false;
|
|
2385
|
-
if (mode === "shadow") {
|
|
2386
|
-
appendDecision(DATA_DIR, {
|
|
2387
|
-
threadId: args.threadId,
|
|
2388
|
-
requestId: args.requestId,
|
|
2389
|
-
botId: args.asker.id,
|
|
2390
|
-
botName: args.asker.name,
|
|
2391
|
-
tool: args.tool,
|
|
2392
|
-
summary: args.summary,
|
|
2393
|
-
decision: reviewed.allow ? "review-would-approve" : "review-would-deny",
|
|
2394
|
-
source: "auto-review-shadow",
|
|
2395
|
-
rule: reviewed.reason,
|
|
2396
|
-
});
|
|
2397
|
-
return false;
|
|
2398
|
-
}
|
|
2399
|
-
if (!reviewed.allow)
|
|
2400
|
-
return false;
|
|
2401
|
-
// The human can answer while review is running. Their click wins before
|
|
2402
|
-
// the provider receives anything and before the audit log claims approval.
|
|
2403
|
-
const card = store.messagesFor(args.threadId).find((message) => message.id === args.messageId)?.card;
|
|
2404
|
-
if (!card || card.answered)
|
|
2405
|
-
return false;
|
|
2406
|
-
let outcome = "unavailable";
|
|
2407
|
-
try {
|
|
2408
|
-
outcome = await args.instance.adapter.respondToRequest(args.threadId, args.requestId, { behavior: "allow" });
|
|
2409
|
-
}
|
|
2410
|
-
catch {
|
|
2411
|
-
return false;
|
|
2412
|
-
}
|
|
2413
|
-
if (outcome === "unavailable")
|
|
2414
|
-
return false;
|
|
2415
|
-
store.appendMessage(args.threadId, {
|
|
2416
|
-
role: "bot",
|
|
2417
|
-
kind: "activity",
|
|
2418
|
-
tool: { name: `review approved ${args.tool}: ${reviewed.reason}`, ok: true },
|
|
2419
|
-
});
|
|
2420
|
-
appendDecision(DATA_DIR, {
|
|
2421
|
-
threadId: args.threadId,
|
|
2422
|
-
requestId: args.requestId,
|
|
2423
|
-
botId: args.asker.id,
|
|
2424
|
-
botName: args.asker.name,
|
|
2425
|
-
tool: args.tool,
|
|
2426
|
-
summary: args.summary,
|
|
2427
|
-
decision: "auto-approved",
|
|
2428
|
-
source: "auto-review",
|
|
2429
|
-
rule: reviewed.reason,
|
|
2430
|
-
});
|
|
2431
|
-
return true;
|
|
2432
|
-
}
|
|
2433
2396
|
bus.subscribe((event) => {
|
|
2434
2397
|
if (shouldIgnoreProviderEvent(event))
|
|
2435
2398
|
return;
|
|
@@ -2440,6 +2403,11 @@ bus.subscribe((event) => {
|
|
|
2440
2403
|
else if (event.type === "turn.completed") {
|
|
2441
2404
|
watchdog.settle(event.threadId);
|
|
2442
2405
|
revokeInternalCapabilityForProviderEvent(event);
|
|
2406
|
+
if (event.turnId) {
|
|
2407
|
+
const owner = directFollowupTurns.complete(event.threadId, event.turnId);
|
|
2408
|
+
if (owner)
|
|
2409
|
+
settleDirectFollowup(owner.generation);
|
|
2410
|
+
}
|
|
2443
2411
|
}
|
|
2444
2412
|
else if (event.type !== "session.exited")
|
|
2445
2413
|
watchdog.touch(event.threadId);
|
|
@@ -2540,6 +2508,9 @@ let localVmProvisionBusy = false;
|
|
|
2540
2508
|
let localVmModeChangeBusy = false;
|
|
2541
2509
|
const activeVpsThreads = new Map();
|
|
2542
2510
|
const boxLifecycleBusyBots = new Set();
|
|
2511
|
+
// A refresh is a reader, not a lifecycle change. Keep its reservation until
|
|
2512
|
+
// the provider settles even if the HTTP client leaves, and share it on retry.
|
|
2513
|
+
const vpsPreviewRequests = new Map();
|
|
2543
2514
|
const orphanBoxLifecycleBusyIds = new Set();
|
|
2544
2515
|
const boxInventoryRequestsBusyIds = new Set();
|
|
2545
2516
|
const computerProviderConfigTransitions = new Set();
|
|
@@ -2588,7 +2559,7 @@ function providerOperationConflict(provider) {
|
|
|
2588
2559
|
if (managedBoxOwners().some((owner) => owner.inUse)) {
|
|
2589
2560
|
return `stop active bot work and computer control before changing ${provider === "box" ? "the Box account" : "the VPS connection"}`;
|
|
2590
2561
|
}
|
|
2591
|
-
if (boxLifecycleBusyBots.size > 0) {
|
|
2562
|
+
if (boxLifecycleBusyBots.size > 0 || vpsPreviewRequests.size > 0) {
|
|
2592
2563
|
return "wait for cloud computer actions to finish before changing provider settings";
|
|
2593
2564
|
}
|
|
2594
2565
|
if (provider === "box") {
|
|
@@ -2642,13 +2613,17 @@ function claimManagedBoxMutation(instance) {
|
|
|
2642
2613
|
}
|
|
2643
2614
|
return claimBotComputerLifecycle(ownerBotId);
|
|
2644
2615
|
}
|
|
2645
|
-
/** One synchronous lane for
|
|
2616
|
+
/** One synchronous lane for cloud lifecycle consumers. Both Settings and
|
|
2646
2617
|
* bot-scoped actions use it, so whichever operation starts first excludes the
|
|
2647
|
-
* other instead of relying on a stale check made before a provider await.
|
|
2648
|
-
|
|
2618
|
+
* other instead of relying on a stale check made before a provider await.
|
|
2619
|
+
* Only opening an existing VPS viewer may coexist with its pending preview. */
|
|
2620
|
+
function claimBotComputerLifecycle(botId, allowPreview = false) {
|
|
2649
2621
|
if (boxLifecycleBusyBots.has(botId)) {
|
|
2650
2622
|
throw Object.assign(new Error("this bot's cloud computer is being changed — wait for it to finish"), { status: 409 });
|
|
2651
2623
|
}
|
|
2624
|
+
if (!allowPreview && vpsPreviewRequests.has(botId)) {
|
|
2625
|
+
throw Object.assign(new Error("a screen preview is still refreshing — wait before changing this computer"), { status: 409 });
|
|
2626
|
+
}
|
|
2652
2627
|
boxLifecycleBusyBots.add(botId);
|
|
2653
2628
|
return () => boxLifecycleBusyBots.delete(botId);
|
|
2654
2629
|
}
|
|
@@ -2828,6 +2803,8 @@ bus.subscribe((event) => {
|
|
|
2828
2803
|
if (bot && event.sessionId && event.providerInstanceId) {
|
|
2829
2804
|
store.setResumeCursor(bot.id, event.providerInstanceId, event.sessionId, event.threadId);
|
|
2830
2805
|
}
|
|
2806
|
+
if (typeof event.model === "string" && event.model)
|
|
2807
|
+
sessionModelByThread.set(event.threadId, event.model);
|
|
2831
2808
|
break;
|
|
2832
2809
|
case "item.completed":
|
|
2833
2810
|
if (event.itemType === "assistant_text") {
|
|
@@ -2907,28 +2884,37 @@ bus.subscribe((event) => {
|
|
|
2907
2884
|
break;
|
|
2908
2885
|
case "request.opened": {
|
|
2909
2886
|
const permission = event.requestType === "permission";
|
|
2910
|
-
//
|
|
2911
|
-
//
|
|
2912
|
-
//
|
|
2913
|
-
//
|
|
2887
|
+
// A permission request here is one the provider left for a person: its
|
|
2888
|
+
// own mode already ran (Ask, Edits, Auto's reviewer, Custom's config).
|
|
2889
|
+
// OpenMausBot decides nothing about the action itself. Only Full access
|
|
2890
|
+
// answers, because that is exactly what the person granted. A QUESTION
|
|
2891
|
+
// always reaches the human — even Full access never invents an answer.
|
|
2914
2892
|
const asker = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
2915
2893
|
const unattended = permission && asker && event.requestId ? isUnattended(asker.id, event.threadId) : false;
|
|
2916
|
-
const effectiveApprovalMode = asker ? approvalModeForTurn(asker, isInternalTurn(event.threadId)
|
|
2894
|
+
const effectiveApprovalMode = asker ? approvalModeForTurn(asker, isInternalTurn(event.threadId)) : "ask";
|
|
2917
2895
|
const verdict = permission && asker && event.requestId
|
|
2918
|
-
? autoVerdict({
|
|
2919
|
-
// Use the same receiving-bot mode as the provider dispatch.
|
|
2920
|
-
approvalMode: effectiveApprovalMode,
|
|
2921
|
-
autoApprove: false,
|
|
2922
|
-
alwaysAllow: asker.alwaysAllow,
|
|
2923
|
-
}, event.tool, event.summary, {
|
|
2924
|
-
unattended,
|
|
2925
|
-
scope: event.approvalScope,
|
|
2926
|
-
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
2927
|
-
// Match the dispatched mode for every provider, including
|
|
2928
|
-
// delegated Custom and unattended Auto downgrades.
|
|
2929
|
-
nativeApproval: requiresNativeApproval(event.provider, effectiveApprovalMode),
|
|
2930
|
-
})
|
|
2896
|
+
? autoVerdict(effectiveApprovalMode, event.tool, { requiresExplicitApproval: event.requiresExplicitApproval })
|
|
2931
2897
|
: null;
|
|
2898
|
+
// Auto's reviewer is the engine's own. Claude accepts `--permission-mode
|
|
2899
|
+
// auto` for any model and starts in Manual without a word when auto is
|
|
2900
|
+
// unavailable (Haiku 4.5, Sonnet 4.5, an org that disabled it), so the
|
|
2901
|
+
// bot asks about everything. Say once per session why, and what stops it.
|
|
2902
|
+
if (permission &&
|
|
2903
|
+
asker &&
|
|
2904
|
+
event.nativeReview === "inactive" &&
|
|
2905
|
+
effectiveApprovalMode === "auto" &&
|
|
2906
|
+
!nativeReviewNoticed.has(event.threadId)) {
|
|
2907
|
+
nativeReviewNoticed.add(event.threadId);
|
|
2908
|
+
const model = sessionModelByThread.get(event.threadId);
|
|
2909
|
+
pushMessage({
|
|
2910
|
+
role: "bot",
|
|
2911
|
+
kind: "activity",
|
|
2912
|
+
tool: {
|
|
2913
|
+
name: `Approve for me: ${providerLabel(event.provider)}'s automatic reviewer is not available${model ? ` for ${model}` : ""}, so this bot asks before each action. Choose a model it supports, or Full access, to stop the prompts.`,
|
|
2914
|
+
ok: true,
|
|
2915
|
+
},
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2932
2918
|
if (verdict?.approve && asker && event.requestId) {
|
|
2933
2919
|
const settled = verdict.approve;
|
|
2934
2920
|
const instance = event.providerInstanceId
|
|
@@ -2964,7 +2950,6 @@ bus.subscribe((event) => {
|
|
|
2964
2950
|
summary,
|
|
2965
2951
|
decision: "auto-approved",
|
|
2966
2952
|
source: verdict.source,
|
|
2967
|
-
rule: verdict.rule,
|
|
2968
2953
|
});
|
|
2969
2954
|
}
|
|
2970
2955
|
catch {
|
|
@@ -2979,17 +2964,8 @@ bus.subscribe((event) => {
|
|
|
2979
2964
|
options: ["Allow", "Deny"],
|
|
2980
2965
|
requestId,
|
|
2981
2966
|
tool,
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
scope: event.approvalScope,
|
|
2985
|
-
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
2986
|
-
}),
|
|
2987
|
-
held: HELD_NOTE[verdict.source === "full-access"
|
|
2988
|
-
? "approval.held.undeliveredFull"
|
|
2989
|
-
: "approval.held.undelivered"],
|
|
2990
|
-
heldCode: verdict.source === "full-access"
|
|
2991
|
-
? "approval.held.undeliveredFull"
|
|
2992
|
-
: "approval.held.undelivered",
|
|
2967
|
+
held: HELD_NOTE["approval.held.undeliveredFull"],
|
|
2968
|
+
heldCode: "approval.held.undeliveredFull",
|
|
2993
2969
|
approvalScope: event.approvalScope,
|
|
2994
2970
|
},
|
|
2995
2971
|
});
|
|
@@ -3003,24 +2979,12 @@ bus.subscribe((event) => {
|
|
|
3003
2979
|
summary,
|
|
3004
2980
|
decision: "card-shown",
|
|
3005
2981
|
source: "auto-fallback",
|
|
3006
|
-
rule: verdict.rule,
|
|
3007
2982
|
});
|
|
3008
2983
|
}
|
|
3009
2984
|
})();
|
|
3010
2985
|
break;
|
|
3011
2986
|
}
|
|
3012
|
-
|
|
3013
|
-
// no mode to explain, but the sandbox note still applies.
|
|
3014
|
-
const heldContext = {
|
|
3015
|
-
source: verdict?.source,
|
|
3016
|
-
permission,
|
|
3017
|
-
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
3018
|
-
mode: asker ? approvalModeForOrigin(approvalModeFor(asker), { peerInitiated: isInternalTurn(event.threadId) }) : "ask",
|
|
3019
|
-
unattended: Boolean(unattended),
|
|
3020
|
-
fullAccessAvailable: asker && !isInternalTurn(event.threadId)
|
|
3021
|
-
? supportsApprovalMode(registry.cliTarget(asker.modelSelection.instanceId)?.driverKind, "full")
|
|
3022
|
-
: false,
|
|
3023
|
-
};
|
|
2987
|
+
const heldContext = { source: verdict?.source, permission };
|
|
3024
2988
|
const message = pushMessage({
|
|
3025
2989
|
role: "bot",
|
|
3026
2990
|
kind: "options",
|
|
@@ -3034,15 +2998,9 @@ bus.subscribe((event) => {
|
|
|
3034
2998
|
options: event.choices?.length ? event.choices : permission ? ["Allow", "Deny"] : [],
|
|
3035
2999
|
requestId: event.requestId,
|
|
3036
3000
|
tool: permission ? event.tool : undefined,
|
|
3037
|
-
// the
|
|
3038
|
-
//
|
|
3039
|
-
|
|
3040
|
-
? rememberableApprovalKey(asker, event.tool, event.summary, {
|
|
3041
|
-
source: verdict?.source,
|
|
3042
|
-
scope: event.approvalScope,
|
|
3043
|
-
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
3044
|
-
})
|
|
3045
|
-
: undefined,
|
|
3001
|
+
// the provider can keep an allow for its session; the app keeps
|
|
3002
|
+
// no grant of its own for a provider's tool
|
|
3003
|
+
allowSession: permission && event.allowSession && !event.requiresExplicitApproval ? true : undefined,
|
|
3046
3004
|
// The text stays for cards saved before heldCode existed, and for
|
|
3047
3005
|
// clients that do not know the key yet.
|
|
3048
3006
|
held: approvalHeldReason(heldContext),
|
|
@@ -3052,37 +3010,8 @@ bus.subscribe((event) => {
|
|
|
3052
3010
|
});
|
|
3053
3011
|
if (event.requestId)
|
|
3054
3012
|
askMessageByRequest.set(`${event.threadId}:${event.requestId}`, message.id);
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
if (permission &&
|
|
3058
|
-
!event.requiresExplicitApproval &&
|
|
3059
|
-
asker &&
|
|
3060
|
-
event.requestId &&
|
|
3061
|
-
shouldReview({
|
|
3062
|
-
source: verdict?.source,
|
|
3063
|
-
mode: reviewMode,
|
|
3064
|
-
approvalMode: approvalModeFor(asker),
|
|
3065
|
-
unattended: Boolean(unattended),
|
|
3066
|
-
approvalScope: event.approvalScope,
|
|
3067
|
-
})) {
|
|
3068
|
-
// Review stays on the provider boundary that opened the request.
|
|
3069
|
-
// Falling back to an arbitrary sibling could disclose action details
|
|
3070
|
-
// to a provider the user did not choose for this bot.
|
|
3071
|
-
const instance = registry.get(event.providerInstanceId ?? asker.modelSelection.instanceId);
|
|
3072
|
-
if (instance?.reviewPermission) {
|
|
3073
|
-
reviewTask = reviewPermissionCard({
|
|
3074
|
-
instance,
|
|
3075
|
-
asker,
|
|
3076
|
-
threadId: event.threadId,
|
|
3077
|
-
requestId: event.requestId,
|
|
3078
|
-
messageId: message.id,
|
|
3079
|
-
tool: event.tool,
|
|
3080
|
-
summary: event.summary,
|
|
3081
|
-
});
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
// Every card that reaches a human is a decision too — "a rule sent
|
|
3085
|
-
// this to you, and here is which one". `question` marks the cards no
|
|
3013
|
+
// Every card that reaches a human is a decision too: "the provider
|
|
3014
|
+
// left this for you, in this mode". `question` marks the cards no
|
|
3086
3015
|
// rule may ever answer; a permission card without a verdict (no known
|
|
3087
3016
|
// asker, or no requestId to answer through) can only mean nothing was
|
|
3088
3017
|
// granted.
|
|
@@ -3095,43 +3024,22 @@ bus.subscribe((event) => {
|
|
|
3095
3024
|
summary: event.summary,
|
|
3096
3025
|
decision: "card-shown",
|
|
3097
3026
|
source: !permission ? "question" : verdict ? verdict.source : "no-grant",
|
|
3098
|
-
rule: verdict?.rule,
|
|
3099
3027
|
unattended: unattended || undefined,
|
|
3100
3028
|
});
|
|
3101
3029
|
// Notify from HERE, not from a separate subscriber on request.opened:
|
|
3102
3030
|
// this is the branch where a card actually reached a human. Anything
|
|
3103
|
-
//
|
|
3104
|
-
|
|
3105
|
-
if (!asker)
|
|
3106
|
-
return;
|
|
3031
|
+
// Full access answered took the early return above and never buzzes.
|
|
3032
|
+
if (asker) {
|
|
3107
3033
|
const card = store.messagesFor(event.threadId).find((candidate) => candidate.id === message.id)?.card;
|
|
3108
|
-
if (
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
};
|
|
3118
|
-
if (reviewTask && reviewMode === "enforce") {
|
|
3119
|
-
// Avoid buzzing the owner for a card the reviewer is about to answer.
|
|
3120
|
-
// A deny, failure, or timeout falls back to the normal notification;
|
|
3121
|
-
// if the human already answered meanwhile, notifyHuman is a no-op.
|
|
3122
|
-
void reviewTask
|
|
3123
|
-
.catch(() => false)
|
|
3124
|
-
.then((approved) => {
|
|
3125
|
-
if (!approved)
|
|
3126
|
-
notifyHuman();
|
|
3127
|
-
});
|
|
3128
|
-
}
|
|
3129
|
-
else {
|
|
3130
|
-
// Watch mode notifies immediately, but its background audit must not
|
|
3131
|
-
// become an unhandled rejection if an unexpected store error occurs.
|
|
3132
|
-
if (reviewTask)
|
|
3133
|
-
void reviewTask.catch(() => false);
|
|
3134
|
-
notifyHuman();
|
|
3034
|
+
if (card && !card.answered) {
|
|
3035
|
+
// the bot is not working now — it is waiting on a person
|
|
3036
|
+
if (bot)
|
|
3037
|
+
store.setTaskActivity(bot.id, event.threadId, "waiting-on-you");
|
|
3038
|
+
else if (asker.busy)
|
|
3039
|
+
store.setActivity(asker.id, "waiting-on-you");
|
|
3040
|
+
const notificationBot = (routineRun && routineSourceOwner(routineRun)?.bot) || asker;
|
|
3041
|
+
notify(buildNotification(permission ? "approval" : "question", notificationBot, (routineRun && routineSourceThread(routineRun)) || event.threadId, event.summary));
|
|
3042
|
+
}
|
|
3135
3043
|
}
|
|
3136
3044
|
break;
|
|
3137
3045
|
}
|
|
@@ -3782,6 +3690,8 @@ bus.subscribe((event) => {
|
|
|
3782
3690
|
drainDelegationWakes();
|
|
3783
3691
|
});
|
|
3784
3692
|
function drainQueuedSends() {
|
|
3693
|
+
if (!followupsReady)
|
|
3694
|
+
return;
|
|
3785
3695
|
drainSteeredMessages(store, (botId, threadId, prompt, userMessage, excludeIds, unattended) =>
|
|
3786
3696
|
// A plain attended turn — no automationSource, no comms depth: exactly
|
|
3787
3697
|
// what typing the same words into an idle bot would run. The one
|
|
@@ -3790,15 +3700,19 @@ function drainQueuedSends() {
|
|
|
3790
3700
|
// Drain just appended the held lines; userMessage keeps startTurn
|
|
3791
3701
|
// from duplicating the last one, and excludeIds drops every drained
|
|
3792
3702
|
// line from the transcript-replay so they are not also in `prompt`.
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3703
|
+
new Promise((resolve, reject) => {
|
|
3704
|
+
void startTurn(botId, prompt, {
|
|
3705
|
+
threadId, userMessage, excludeMessageIds: excludeIds, unattended, onTurnSettled: resolve,
|
|
3706
|
+
}).catch((err) => {
|
|
3707
|
+
store.appendMessage(threadId, {
|
|
3708
|
+
role: "bot", kind: "activity",
|
|
3709
|
+
tool: {
|
|
3710
|
+
name: `error: queued message could not start — ${(err instanceof Error ? err.message : String(err)).slice(0, 120)}`,
|
|
3711
|
+
ok: false,
|
|
3712
|
+
},
|
|
3713
|
+
});
|
|
3714
|
+
resolve();
|
|
3715
|
+
}).catch(reject);
|
|
3802
3716
|
}),
|
|
3803
3717
|
// Provider completion can precede its dispatch promise: keep the queue
|
|
3804
3718
|
// intact until that exact handshake releases its runtime-only claim.
|
|
@@ -4203,6 +4117,8 @@ async function startTurn(botId, text, opts) {
|
|
|
4203
4117
|
const resourceOwner = { threadId, generation: dispatchClaimId };
|
|
4204
4118
|
turnResourceOwners.set(threadId, resourceOwner);
|
|
4205
4119
|
directTurnGenerationByThread.set(threadId, dispatchClaimId);
|
|
4120
|
+
if (opts?.onTurnSettled)
|
|
4121
|
+
directFollowupSettlers.set(dispatchClaimId, { threadId, settle: opts.onTurnSettled });
|
|
4206
4122
|
directTurnDispatchClaims.set(threadId, { id: dispatchClaimId, botId, threadId, phase: "setup" });
|
|
4207
4123
|
directTurnBots.set(threadId, bot);
|
|
4208
4124
|
beginInternalCapabilityGeneration(threadId, dispatchClaimId);
|
|
@@ -4643,6 +4559,11 @@ async function startTurn(botId, text, opts) {
|
|
|
4643
4559
|
throw new DirectTurnSetupCancelled("turn stopped during provider setup");
|
|
4644
4560
|
}
|
|
4645
4561
|
bindInternalCapabilityToProviderTurn(threadId, dispatchClaimId, dispatch.value.turnId);
|
|
4562
|
+
if (directFollowupSettlers.has(dispatchClaimId) && dispatch.value.turnId &&
|
|
4563
|
+
!directFollowupTurns.bind(threadId, dispatchClaimId, dispatch.value.turnId)) {
|
|
4564
|
+
// This exact queued turn completed before its dispatch ACK arrived.
|
|
4565
|
+
settleDirectFollowup(dispatchClaimId);
|
|
4566
|
+
}
|
|
4646
4567
|
clearDirectTurnDispatch(threadId, dispatchClaimId);
|
|
4647
4568
|
// dispatched: the rewind is spent, and the old cursors are dead
|
|
4648
4569
|
if (rewound)
|
|
@@ -4676,6 +4597,7 @@ async function startTurn(botId, text, opts) {
|
|
|
4676
4597
|
}
|
|
4677
4598
|
}
|
|
4678
4599
|
catch (e) {
|
|
4600
|
+
settleDirectFollowup(dispatchClaimId);
|
|
4679
4601
|
clearCancelledProviderHandshake(threadId, `direct:${dispatchClaimId}`);
|
|
4680
4602
|
clearDirectTurnDispatch(threadId, dispatchClaimId);
|
|
4681
4603
|
revokeInternalCapabilityGeneration(threadId, dispatchClaimId);
|
|
@@ -5268,6 +5190,7 @@ const webhooks = new WebhookManager({
|
|
|
5268
5190
|
return !bot ? "missing" : bot.busy ? "busy" : "ready";
|
|
5269
5191
|
},
|
|
5270
5192
|
enqueue: (input) => routines.enqueueWebhook(input),
|
|
5193
|
+
findRun: (webhookId, deliveryId) => routines.webhookRunReceipt(webhookId, deliveryId),
|
|
5271
5194
|
cancelQueued: (webhookId, message) => routines.cancelQueuedWebhook(webhookId, message),
|
|
5272
5195
|
pendingRuns: (webhookId) => routines.activeWebhookRunCount(webhookId),
|
|
5273
5196
|
});
|
|
@@ -5379,7 +5302,7 @@ setupRetry = 0) {
|
|
|
5379
5302
|
}
|
|
5380
5303
|
revokeInternalCapabilitiesForThread(threadId);
|
|
5381
5304
|
spoken.add(botId);
|
|
5382
|
-
const preparedApprovalMode = approvalModeForTurn(bot, false
|
|
5305
|
+
const preparedApprovalMode = approvalModeForTurn(bot, false);
|
|
5383
5306
|
const preparedSelection = { ...bot.modelSelection };
|
|
5384
5307
|
const preparedComposio = bot.composio;
|
|
5385
5308
|
const instance = registry.get(bot.modelSelection.instanceId);
|
|
@@ -5502,7 +5425,7 @@ setupRetry = 0) {
|
|
|
5502
5425
|
if (!readyGroup || !stillOwnsThread || !readyGroup.memberIds.includes(readyBot.id))
|
|
5503
5426
|
return false;
|
|
5504
5427
|
const setupChanged = registry.get(preparedSelection.instanceId) !== instance ||
|
|
5505
|
-
approvalModeForTurn(readyBot, false
|
|
5428
|
+
approvalModeForTurn(readyBot, false) !== preparedApprovalMode ||
|
|
5506
5429
|
readyBot.modelSelection.instanceId !== preparedSelection.instanceId ||
|
|
5507
5430
|
readyBot.modelSelection.model !== preparedSelection.model ||
|
|
5508
5431
|
readyBot.modelSelection.effort !== preparedSelection.effort ||
|
|
@@ -5809,7 +5732,7 @@ setupRetry = 0) {
|
|
|
5809
5732
|
threadId,
|
|
5810
5733
|
text,
|
|
5811
5734
|
images: turnImages,
|
|
5812
|
-
approvalMode: approvalModeForTurn(readyBot, false
|
|
5735
|
+
approvalMode: approvalModeForTurn(readyBot, false),
|
|
5813
5736
|
system: roomSystem.text,
|
|
5814
5737
|
systemStable: roomSystem.stable,
|
|
5815
5738
|
systemVolatile: roomSystem.volatile,
|
|
@@ -6431,6 +6354,8 @@ function startGroupTurn(groupId, text, replyTo, sendId, channelMode = "chat", qu
|
|
|
6431
6354
|
return message;
|
|
6432
6355
|
}
|
|
6433
6356
|
function drainQueuedChannelSends() {
|
|
6357
|
+
if (!followupsReady)
|
|
6358
|
+
return;
|
|
6434
6359
|
drainChannelMessages((groupId) => {
|
|
6435
6360
|
const group = store.group(groupId);
|
|
6436
6361
|
return group ? groupIsWorking(group) : false;
|
|
@@ -6442,9 +6367,12 @@ function drainQueuedChannelSends() {
|
|
|
6442
6367
|
if (!group || !ownsThread)
|
|
6443
6368
|
return;
|
|
6444
6369
|
try {
|
|
6445
|
-
startGroupTurn(groupId, text, resolveReplyTarget(threadId, replyToId), sendId, mode, id, { via });
|
|
6370
|
+
startGroupTurn(groupId, text, resolveReplyTarget(threadId, replyToId), sendId, mode, id, { via, threadId });
|
|
6446
6371
|
}
|
|
6447
6372
|
catch (error) {
|
|
6373
|
+
if (!store.messagesFor(threadId).some((message) => message.queueId === id && message.role === "user")) {
|
|
6374
|
+
store.appendMessage(threadId, { role: "user", kind: "text", text, replyToId, sendId, channelMode: mode, queueId: id, via });
|
|
6375
|
+
}
|
|
6448
6376
|
store.appendMessage(threadId, {
|
|
6449
6377
|
role: "bot",
|
|
6450
6378
|
kind: "activity",
|
|
@@ -6457,6 +6385,7 @@ function drainQueuedChannelSends() {
|
|
|
6457
6385
|
// A message with no eligible responder creates no operation. Continue
|
|
6458
6386
|
// draining instead of leaving later user messages behind it forever.
|
|
6459
6387
|
queueMicrotask(drainQueuedChannelSends);
|
|
6388
|
+
return groupQueues.get(groupId);
|
|
6460
6389
|
});
|
|
6461
6390
|
}
|
|
6462
6391
|
function sameCalendarRoster(group, botIds) {
|
|
@@ -7364,6 +7293,14 @@ function configStatus() {
|
|
|
7364
7293
|
showToolCalls: showToolCallsEnabled(cfg),
|
|
7365
7294
|
browser: builtInBrowserEnabled(cfg),
|
|
7366
7295
|
},
|
|
7296
|
+
// first-run progress — not a secret; the app decides whether to show
|
|
7297
|
+
// the welcome tour from this, never from browser storage
|
|
7298
|
+
onboarding: {
|
|
7299
|
+
completedAt: cfg.onboarding?.completedAt ?? "",
|
|
7300
|
+
version: cfg.onboarding?.version ?? 0,
|
|
7301
|
+
reelSeen: cfg.onboarding?.reelSeen === true,
|
|
7302
|
+
hintsSeen: cfg.onboarding?.hintsSeen ?? [],
|
|
7303
|
+
},
|
|
7367
7304
|
// Which browser this server can give bots: the desktop app's surface,
|
|
7368
7305
|
// the agent-browser engine, or nothing yet (with the reason).
|
|
7369
7306
|
browserEngine: browserEngineSummary(),
|
|
@@ -7482,6 +7419,7 @@ async function reloadProviders() {
|
|
|
7482
7419
|
});
|
|
7483
7420
|
store.setTaskActivity(botId, threadId, "idle");
|
|
7484
7421
|
}
|
|
7422
|
+
settleDirectFollowup(owner?.generation);
|
|
7485
7423
|
retryDelegationsWaitingOn(botId);
|
|
7486
7424
|
}
|
|
7487
7425
|
for (const [threadId, speaker] of rooms) {
|
|
@@ -7634,7 +7572,7 @@ const workspaceBackupRoutes = createWorkspaceBackupRoutes({
|
|
|
7634
7572
|
idle: () => !providerFleetReloading && !providerAuthSessions.active && !browserEngineInstall &&
|
|
7635
7573
|
!routines?.isTicking && !calendarCalls?.isTicking &&
|
|
7636
7574
|
!localVmImageBusy && !localVmProvisionBusy && !localVmModeChangeBusy &&
|
|
7637
|
-
!localVmLifecycleBusy.size && !boxLifecycleBusyBots.size && !orphanBoxLifecycleBusyIds.size &&
|
|
7575
|
+
!localVmLifecycleBusy.size && !boxLifecycleBusyBots.size && !vpsPreviewRequests.size && !orphanBoxLifecycleBusyIds.size &&
|
|
7638
7576
|
!computerProviderConfigTransitions.size && !checkpointRestoreLeases.size &&
|
|
7639
7577
|
store.bots.every((bot) => !botHasActiveTurn(bot.id) && !routines?.activeRunForBot(bot.id) && !computerControl.snapshot(bot.id).held) &&
|
|
7640
7578
|
store.groups.every((group) => !groupIsWorking(group)),
|
|
@@ -10499,6 +10437,9 @@ const handleRequest = async (req, res) => {
|
|
|
10499
10437
|
}
|
|
10500
10438
|
const receipt = await sendSequencer.run(sendId ? `group:${group.id}:${threadId}:${sendId}` : undefined, sendFingerprint(text, replyTo?.id, channelMode), async () => {
|
|
10501
10439
|
if (sendId) {
|
|
10440
|
+
if (cancelledChatFollowup("channel", group.id, threadId, sendId)) {
|
|
10441
|
+
throw Object.assign(new Error("this queued sendId was cancelled; send a new message to try again"), { status: 409 });
|
|
10442
|
+
}
|
|
10502
10443
|
const accepted = acceptedSendMatch(store.messagesFor(threadId), sendId, text, replyTo?.id, channelMode);
|
|
10503
10444
|
if (accepted.kind === "conflict") {
|
|
10504
10445
|
throw Object.assign(new Error("sendId already belongs to another message"), { status: 409 });
|
|
@@ -11094,12 +11035,6 @@ const handleRequest = async (req, res) => {
|
|
|
11094
11035
|
error: "This approval-level change can only be made from the packaged desktop app",
|
|
11095
11036
|
});
|
|
11096
11037
|
}
|
|
11097
|
-
if (body.autoReview !== undefined) {
|
|
11098
|
-
if (body.autoReview !== "off" && body.autoReview !== "shadow" && body.autoReview !== "enforce") {
|
|
11099
|
-
return json(res, 400, { error: "autoReview must be off, shadow, or enforce" });
|
|
11100
|
-
}
|
|
11101
|
-
patch.autoReview = body.autoReview;
|
|
11102
|
-
}
|
|
11103
11038
|
// "Auto on this Mac" hands a bot the user's real session, so the grant
|
|
11104
11039
|
// must prove a human saw the warning. The desktop dialog is the only
|
|
11105
11040
|
// caller that sends acknowledgeLocalAuto; without it a PATCH that would
|
|
@@ -11455,6 +11390,7 @@ const handleRequest = async (req, res) => {
|
|
|
11455
11390
|
// staged provider images into a message, so dispose them here.
|
|
11456
11391
|
for (const task of store.tasks(bot.id)) {
|
|
11457
11392
|
purgeGeneratedImagesForThread(task.threadId);
|
|
11393
|
+
settleDirectFollowup(directTurnGenerationByThread.get(task.threadId));
|
|
11458
11394
|
directTurnGenerationByThread.delete(task.threadId);
|
|
11459
11395
|
directTurnBots.delete(task.threadId);
|
|
11460
11396
|
}
|
|
@@ -11998,6 +11934,9 @@ const handleRequest = async (req, res) => {
|
|
|
11998
11934
|
const replyTo = resolveReplyTarget(threadId, body.replyToId);
|
|
11999
11935
|
const receipt = await sendSequencer.run(sendId ? `bot:${bot.id}:${threadId}:${sendId}` : undefined, sendFingerprint(text, replyTo?.id), async () => {
|
|
12000
11936
|
if (sendId) {
|
|
11937
|
+
if (cancelledChatFollowup("bot", bot.id, threadId, sendId)) {
|
|
11938
|
+
throw Object.assign(new Error("this queued sendId was cancelled; send a new message to try again"), { status: 409 });
|
|
11939
|
+
}
|
|
12001
11940
|
const accepted = acceptedSendMatch(store.messagesFor(threadId), sendId, text, replyTo?.id);
|
|
12002
11941
|
if (accepted.kind === "conflict") {
|
|
12003
11942
|
throw Object.assign(new Error("sendId already belongs to another message"), { status: 409 });
|
|
@@ -12197,7 +12136,7 @@ const handleRequest = async (req, res) => {
|
|
|
12197
12136
|
resolvePeerComms(approvalBus, String(body.requestId), behavior)) {
|
|
12198
12137
|
return json(res, 200, { ok: true, outcome: behavior === "allow" ? "allowed-once" : "rejected" });
|
|
12199
12138
|
}
|
|
12200
|
-
const outcome = await answerRequest(bot.threadId, bot.modelSelection.instanceId, String(body.requestId), behavior, body.message, { id: bot.id, name: bot.name });
|
|
12139
|
+
const outcome = await answerRequest(bot.threadId, bot.modelSelection.instanceId, String(body.requestId), behavior, body.message, { id: bot.id, name: bot.name }, body.always === true);
|
|
12201
12140
|
return json(res, 200, { ok: true, outcome });
|
|
12202
12141
|
}
|
|
12203
12142
|
// Answer by THREAD, so a request raised inside a room can be answered
|
|
@@ -12282,7 +12221,7 @@ const handleRequest = async (req, res) => {
|
|
|
12282
12221
|
if (!owner && !pending)
|
|
12283
12222
|
return json(res, 404, { error: "nothing is waiting on an answer in this conversation" });
|
|
12284
12223
|
const requestOwner = owner ? botForThread(owner.id, threadId) : null;
|
|
12285
|
-
const outcome = await answerRequest(threadId, requestOwner?.modelSelection.instanceId ?? "", requestId, behavior, body.message, owner ? { id: owner.id, name: owner.name } : undefined);
|
|
12224
|
+
const outcome = await answerRequest(threadId, requestOwner?.modelSelection.instanceId ?? "", requestId, behavior, body.message, owner ? { id: owner.id, name: owner.name } : undefined, body.always === true);
|
|
12286
12225
|
return json(res, 200, { ok: true, outcome });
|
|
12287
12226
|
}
|
|
12288
12227
|
m = path.match(/^\/api\/bots\/([\w-]+)\/interrupt$/);
|
|
@@ -12539,6 +12478,7 @@ const handleRequest = async (req, res) => {
|
|
|
12539
12478
|
const updated = store.deleteTask(m[1], m[2]);
|
|
12540
12479
|
if (!updated)
|
|
12541
12480
|
return json(res, 400, { error: "a bot keeps at least one task" });
|
|
12481
|
+
settleDirectFollowup(directTurnGenerationByThread.get(m[2]));
|
|
12542
12482
|
rejectDeletedThreadSkillStages(stagedSkillCleanups);
|
|
12543
12483
|
const fresh = botWithThread(updated);
|
|
12544
12484
|
broadcast({ kind: "bot", bot: fresh });
|
|
@@ -13634,19 +13574,10 @@ const handleRequest = async (req, res) => {
|
|
|
13634
13574
|
browserReferenceCleanupError = error;
|
|
13635
13575
|
}
|
|
13636
13576
|
}
|
|
13637
|
-
// Provider keys change the fleet. Profile, language, voice, VPS,
|
|
13638
|
-
//
|
|
13639
|
-
// should not interrupt in-flight turns.
|
|
13640
|
-
const reloadKeys =
|
|
13641
|
-
key !== "language" &&
|
|
13642
|
-
key !== "tts" &&
|
|
13643
|
-
key !== "imageGen" &&
|
|
13644
|
-
key !== "vps" &&
|
|
13645
|
-
key !== "rooms" &&
|
|
13646
|
-
key !== "threads" &&
|
|
13647
|
-
key !== "localVm" &&
|
|
13648
|
-
key !== "features" &&
|
|
13649
|
-
key !== "browserProfiles");
|
|
13577
|
+
// Provider keys change the fleet. Profile, language, voice, VPS, room
|
|
13578
|
+
// timeout, and onboarding progress changes do not rebuild it: no driver
|
|
13579
|
+
// reads them, and they should not interrupt in-flight turns.
|
|
13580
|
+
const reloadKeys = providerReloadKeys(patch);
|
|
13650
13581
|
// Config is already durable. A provider credential or runtime change
|
|
13651
13582
|
// invalidates every old child immediately, including when browser
|
|
13652
13583
|
// cleanup below has to await Electron before reloadProviders begins.
|
|
@@ -14032,7 +13963,19 @@ const handleRequest = async (req, res) => {
|
|
|
14032
13963
|
return json(res, 409, { error: "this bot's cloud computer is being changed — wait for it to finish" });
|
|
14033
13964
|
}
|
|
14034
13965
|
if (bot.cloudBackend === "vps") {
|
|
14035
|
-
|
|
13966
|
+
if (m[2] === "screenshot") {
|
|
13967
|
+
let preview = vpsPreviewRequests.get(botId);
|
|
13968
|
+
if (!preview) {
|
|
13969
|
+
preview = vps.vpsComputerScreenshot(cfg, botId).finally(() => {
|
|
13970
|
+
vpsPreviewRequests.delete(botId);
|
|
13971
|
+
});
|
|
13972
|
+
vpsPreviewRequests.set(botId, preview);
|
|
13973
|
+
}
|
|
13974
|
+
return json(res, 200, await preview);
|
|
13975
|
+
}
|
|
13976
|
+
// Opening the existing SSH viewer can coexist with a capture. Start,
|
|
13977
|
+
// stop, remove and Settings deletion still exclude pending previews.
|
|
13978
|
+
const releaseComputerLifecycle = claimBotComputerLifecycle(botId, m[2] === "join");
|
|
14036
13979
|
try {
|
|
14037
13980
|
if (m[2] === "exec") {
|
|
14038
13981
|
return json(res, 409, { error: "the VPS console is available to the bot through its scoped computer tools" });
|
|
@@ -14046,8 +13989,6 @@ const handleRequest = async (req, res) => {
|
|
|
14046
13989
|
if (m[2] === "join") {
|
|
14047
13990
|
return json(res, 200, await vps.vpsComputerJoin(cfg, botId));
|
|
14048
13991
|
}
|
|
14049
|
-
if (m[2] === "screenshot")
|
|
14050
|
-
return json(res, 200, await vps.vpsComputerScreenshot(cfg, botId));
|
|
14051
13992
|
const action = m[2] === "provision" ? "provision" : m[2] === "remove" ? "remove" : "stop";
|
|
14052
13993
|
return json(res, 200, await vps.vpsComputerAction(action, cfg, botId));
|
|
14053
13994
|
}
|
|
@@ -14132,8 +14073,45 @@ try {
|
|
|
14132
14073
|
catch (error) {
|
|
14133
14074
|
console.warn(`attachments: startup partial cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
14134
14075
|
}
|
|
14076
|
+
// A dispatch claim is deliberately committed before transcript/provider work.
|
|
14077
|
+
// If we died after that point, its outcome is unknown: recover the user's words
|
|
14078
|
+
// and a review notice, never hand them to a model for a second execution.
|
|
14079
|
+
for (const row of chatFollowups()) {
|
|
14080
|
+
if (row.status !== "dispatching" && row.status !== "interrupted")
|
|
14081
|
+
continue;
|
|
14082
|
+
const owned = row.kind === "bot"
|
|
14083
|
+
? Boolean(store.taskByThread(row.ownerId, row.threadId))
|
|
14084
|
+
: Boolean(store.groupByThread(row.threadId)?.id === row.ownerId);
|
|
14085
|
+
if (!owned) {
|
|
14086
|
+
settleChatFollowups([row.id], "cancelled");
|
|
14087
|
+
continue;
|
|
14088
|
+
}
|
|
14089
|
+
settleChatFollowups([row.id], "interrupted");
|
|
14090
|
+
const messages = store.messagesFor(row.threadId);
|
|
14091
|
+
if (!messages.some((message) => message.queueId === row.id && message.role === "user")) {
|
|
14092
|
+
store.appendMessage(row.threadId, {
|
|
14093
|
+
role: "user", kind: "text", text: row.payload.text, replyToId: row.payload.replyToId,
|
|
14094
|
+
sendId: row.payload.sendId, queueId: row.id,
|
|
14095
|
+
...(row.kind === "channel" ? { channelMode: row.payload.mode, via: row.payload.via } : {}),
|
|
14096
|
+
});
|
|
14097
|
+
}
|
|
14098
|
+
if (!messages.some((message) => message.queueId === row.id && message.kind === "activity")) {
|
|
14099
|
+
store.appendMessage(row.threadId, {
|
|
14100
|
+
role: "bot", kind: "activity", queueId: row.id,
|
|
14101
|
+
tool: { name: "Queued follow-up interrupted by restart or restore — it may have already run. Review the result before sending it again.", ok: false },
|
|
14102
|
+
});
|
|
14103
|
+
}
|
|
14104
|
+
// The FULL-sync retirement also flushes both transcript writes. Retrying
|
|
14105
|
+
// this sendId now finds the canonical message, without a permanent journal scan.
|
|
14106
|
+
settleChatFollowups([row.id], null);
|
|
14107
|
+
}
|
|
14108
|
+
restoreSteeredMessages();
|
|
14109
|
+
restoreChannelMessages();
|
|
14135
14110
|
server.listen(PORT, "127.0.0.1", () => {
|
|
14136
14111
|
console.log(`openmausbot server on http://127.0.0.1:${PORT}`);
|
|
14112
|
+
followupsReady = true;
|
|
14113
|
+
drainQueuedSends();
|
|
14114
|
+
drainQueuedChannelSends();
|
|
14137
14115
|
// Startup work uses the same turn dispatcher and local tool endpoint as
|
|
14138
14116
|
// ordinary chat. Start only once every registry is initialized and the
|
|
14139
14117
|
// endpoint is listening; earlier dispatch can hit uninitialized bindings.
|
|
@@ -14171,6 +14149,7 @@ if (TUNNEL_SOCKET) {
|
|
|
14171
14149
|
const gracefulShutdown = createGracefulShutdown({
|
|
14172
14150
|
cleanup: [
|
|
14173
14151
|
() => {
|
|
14152
|
+
followupsReady = false;
|
|
14174
14153
|
// Child MCP processes and the HTTP listener can remain alive while the
|
|
14175
14154
|
// asynchronous shutdown jobs drain. Invalidate their turn bearers before
|
|
14176
14155
|
// any cleanup function reaches an await.
|