openmausbot 0.1.73 → 0.1.75
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-BYbK7ber.js → index-BTlyRwLl.js} +1 -1
- package/dist/assets/index-CeMbvsgx.js +305 -0
- package/dist/assets/index-DH7Zax6L.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +9 -0
- package/dist-server/index.js +1402 -1048
- package/dist-server/mcp-gate.js +58 -16
- package/dist-server/openmausbot.js +543 -271
- package/dist-server/pair-cli.js +543 -271
- package/dist-server/permission-proxy.js +119 -14
- package/dist-server/server/auto-approve.js +44 -205
- 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 +58 -4
- package/dist-server/server/drivers/acp/grok.js +15 -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 +113 -13
- package/dist-server/server/drivers/codex.js +37 -15
- package/dist-server/server/drivers/retry.js +4 -0
- package/dist-server/server/engine-install.js +9 -5
- package/dist-server/server/index.js +265 -233
- package/dist-server/server/message-db.js +70 -3
- package/dist-server/server/permission-proxy.js +90 -15
- package/dist-server/server/procs.js +82 -23
- package/dist-server/server/routines.js +67 -8
- package/dist-server/server/setup-mode.js +6 -7
- package/dist-server/server/steer-queue.js +27 -7
- package/dist-server/server/store.js +19 -0
- package/dist-server/server/team-manifest.js +1 -1
- package/dist-server/server/thread-events.js +10 -1
- package/dist-server/server/webhooks.js +4 -1
- package/dist-server/server/workspace-backup.js +24 -9
- package/dist-server/server/workspace.js +13 -0
- package/dist-server/shared/approval-mode.js +11 -10
- package/dist-server/shared/ask-question.js +172 -0
- package/dist-server/shared/provider-safety.js +6 -0
- package/dist-server/vps-container-mcp.js +9 -0
- package/package.json +1 -1
- package/dist/assets/index-BLJ7byG3.js +0 -296
- package/dist/assets/index-De2TRLxu.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";
|
|
@@ -76,7 +75,7 @@ import { buildRecoveryText, buildTurnContext, engineIsFresh } from "./turn-conte
|
|
|
76
75
|
import { extractTurnImages } from "./turn-images.js";
|
|
77
76
|
import { TurnWatchdog } from "./turn-watchdog.js";
|
|
78
77
|
import { TurnResources, workspaceResource } from "./turn-resources.js";
|
|
79
|
-
import { ensureWorkspace, ensureTaskWorkspace, updateMemory, appendMemoryLog, isMemoryTopicName, memorySystemPrompt, memorySourceLabel, searchMemoryFiles, SESSION_SEARCH_SYSTEM_PROMPT, workspaceDir, } from "./workspace.js";
|
|
78
|
+
import { ensureWorkspace, ensureTaskWorkspace, workspaceLocationsPrompt, updateMemory, appendMemoryLog, isMemoryTopicName, memorySystemPrompt, memorySourceLabel, searchMemoryFiles, SESSION_SEARCH_SYSTEM_PROMPT, workspaceDir, } from "./workspace.js";
|
|
80
79
|
import { readMemoryTopic } from "./workspace.js";
|
|
81
80
|
import { MEMORY_INDEX, MemoryStoreError, memoryCapacity, memoryOverview, openMemoryLocation, readMemoryDoc, } from "./memory-store.js";
|
|
82
81
|
import { beginMemoryTurn, endMemoryTurn, flushAllMemoryJournals, flushMemoryJournal, journalMemoryDelete, journalMemoryWrite, readMemoryJournal, revertMemoryChange, } from "./memory-journal.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
|
|
@@ -1203,6 +1213,14 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1203
1213
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
1204
1214
|
return false;
|
|
1205
1215
|
const message = raw;
|
|
1216
|
+
const threadCanReceiveGrant = (bot) => {
|
|
1217
|
+
const threadId = bot.approvalGrant?.threadId;
|
|
1218
|
+
if (!threadId)
|
|
1219
|
+
return true;
|
|
1220
|
+
const target = store.projectBotForTask(bot.id, threadId);
|
|
1221
|
+
return Boolean(target && !threadBusy(bot.id, threadId) &&
|
|
1222
|
+
registry.cliTarget(target.modelSelection.instanceId)?.driverKind === registry.cliTarget(bot.modelSelection.instanceId)?.driverKind);
|
|
1223
|
+
};
|
|
1206
1224
|
if (message.type === "approval-trusted-mode-commit") {
|
|
1207
1225
|
const requestId = typeof message.requestId === "string" && /^[A-Za-z0-9_-]{1,80}$/.test(message.requestId)
|
|
1208
1226
|
? message.requestId
|
|
@@ -1221,7 +1239,11 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1221
1239
|
bot.approvalGrant.phase === "committed" &&
|
|
1222
1240
|
bot.approvalMode === mode &&
|
|
1223
1241
|
!bot.busy &&
|
|
1242
|
+
threadCanReceiveGrant(bot) &&
|
|
1224
1243
|
supportsApprovalMode(registry.cliTarget(bot.modelSelection.instanceId)?.driverKind, mode)) {
|
|
1244
|
+
if (bot.approvalGrant.threadId) {
|
|
1245
|
+
store.patchTask(botId, bot.approvalGrant.threadId, { approvalMode: mode, autoApprove: false });
|
|
1246
|
+
}
|
|
1225
1247
|
store.patchBot(botId, { approvalGrant: undefined });
|
|
1226
1248
|
}
|
|
1227
1249
|
else if (bot?.approvalGrant?.requestId === requestId) {
|
|
@@ -1262,7 +1284,7 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1262
1284
|
return true;
|
|
1263
1285
|
}
|
|
1264
1286
|
store.patchBot(botId, {
|
|
1265
|
-
approvalGrant: { requestId, mode, phase: "confirmed" },
|
|
1287
|
+
approvalGrant: { ...bot.approvalGrant, requestId, mode, phase: "confirmed" },
|
|
1266
1288
|
});
|
|
1267
1289
|
confirm(true);
|
|
1268
1290
|
return true;
|
|
@@ -1311,7 +1333,7 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1311
1333
|
}
|
|
1312
1334
|
// Still inert: Electron must receive this acknowledgement and request
|
|
1313
1335
|
// finalization before the durable mode can affect any turn.
|
|
1314
|
-
store.patchBot(botId, { approvalGrant: { requestId, mode, phase: "activated" } });
|
|
1336
|
+
store.patchBot(botId, { approvalGrant: { ...bot.approvalGrant, requestId, mode, phase: "activated" } });
|
|
1315
1337
|
activate(true);
|
|
1316
1338
|
return true;
|
|
1317
1339
|
}
|
|
@@ -1349,10 +1371,11 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1349
1371
|
bot.approvalGrant.phase === "activated" &&
|
|
1350
1372
|
bot.approvalMode === mode &&
|
|
1351
1373
|
!bot.busy &&
|
|
1374
|
+
threadCanReceiveGrant(bot) &&
|
|
1352
1375
|
supportsApprovalMode(registry.cliTarget(bot.modelSelection.instanceId)?.driverKind, mode)) {
|
|
1353
1376
|
// Durable but still inert. Electron must observe this exact ACK before
|
|
1354
1377
|
// sending the one-way commit release that clears the journal.
|
|
1355
|
-
store.patchBot(botId, { approvalGrant: { requestId, mode, phase: "committed" } });
|
|
1378
|
+
store.patchBot(botId, { approvalGrant: { ...bot.approvalGrant, requestId, mode, phase: "committed" } });
|
|
1356
1379
|
finalize(true);
|
|
1357
1380
|
return true;
|
|
1358
1381
|
}
|
|
@@ -1396,6 +1419,20 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1396
1419
|
return true;
|
|
1397
1420
|
}
|
|
1398
1421
|
const currentMode = approvalModeFor(existing);
|
|
1422
|
+
const threadId = message.threadId;
|
|
1423
|
+
if (threadId !== undefined) {
|
|
1424
|
+
const target = typeof threadId === "string" && /^[\w-]{1,128}$/.test(threadId)
|
|
1425
|
+
? store.projectBotForTask(botId, threadId) : null;
|
|
1426
|
+
if (!target || (mode !== "full" && mode !== "custom") || currentMode !== mode || existing.approvalGrant) {
|
|
1427
|
+
respond({ ok: false, error: "Choose this bot's approval level in bot settings before applying it to an existing thread" });
|
|
1428
|
+
return true;
|
|
1429
|
+
}
|
|
1430
|
+
if (threadBusy(botId, threadId) ||
|
|
1431
|
+
registry.cliTarget(target.modelSelection.instanceId)?.driverKind !== registry.cliTarget(existing.modelSelection.instanceId)?.driverKind) {
|
|
1432
|
+
respond({ ok: false, error: "Stop this thread and use the bot's provider before applying its approval level" });
|
|
1433
|
+
return true;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1399
1436
|
const emergencyDowngrade = existing.busy && isEmergencyApprovalDowngrade(currentMode, mode);
|
|
1400
1437
|
const clearsPendingElevation = mode === "ask" && existing.approvalGrant !== undefined;
|
|
1401
1438
|
if (existing.busy && !emergencyDowngrade && !clearsPendingElevation) {
|
|
@@ -1422,7 +1459,7 @@ function handleDesktopTrustedApprovalMessage(raw) {
|
|
|
1422
1459
|
approvalMode: mode,
|
|
1423
1460
|
autoApprove: mode === "auto",
|
|
1424
1461
|
approvalGrant: mode === "full" || mode === "custom"
|
|
1425
|
-
? { requestId, mode, phase: "prepared" }
|
|
1462
|
+
? { requestId, mode, phase: "prepared", ...(typeof threadId === "string" ? { threadId } : {}) }
|
|
1426
1463
|
: undefined,
|
|
1427
1464
|
});
|
|
1428
1465
|
if (!updated) {
|
|
@@ -2162,7 +2199,9 @@ const askMessageByRequest = new Map(); // threadId:requestId -> messageId
|
|
|
2162
2199
|
* the engine has no asks — is fail-closed: the action was never run. The
|
|
2163
2200
|
* card is settled and a chip says so, instead of the answer vanishing into
|
|
2164
2201
|
* a 500 while the card sits open forever. */
|
|
2165
|
-
async function answerRequest(threadId, instanceId, requestId, behavior, message, decidedFor
|
|
2202
|
+
async function answerRequest(threadId, instanceId, requestId, behavior, message, decidedFor,
|
|
2203
|
+
/** "Always allow this session": the provider keeps the allow, not the app */
|
|
2204
|
+
always) {
|
|
2166
2205
|
// Snapshot the card BEFORE delivering the answer: a delivered answer
|
|
2167
2206
|
// resolves the request synchronously through the fold, which consumes
|
|
2168
2207
|
// the askMessageByRequest entry — by the time the await returns, nobody
|
|
@@ -2179,12 +2218,21 @@ async function answerRequest(threadId, instanceId, requestId, behavior, message,
|
|
|
2179
2218
|
let outcome = "unavailable";
|
|
2180
2219
|
if (instance) {
|
|
2181
2220
|
try {
|
|
2182
|
-
outcome = await instance.adapter.respondToRequest(threadId, requestId, { behavior, message });
|
|
2221
|
+
outcome = await instance.adapter.respondToRequest(threadId, requestId, { behavior, message, always: always && behavior === "allow" });
|
|
2183
2222
|
}
|
|
2184
2223
|
catch {
|
|
2185
2224
|
outcome = "unavailable";
|
|
2186
2225
|
}
|
|
2187
2226
|
}
|
|
2227
|
+
// An answered question keeps its words. `request.resolved` only records
|
|
2228
|
+
// the behavior, so without this the card reads "answer" forever and the
|
|
2229
|
+
// person can no longer see what they told the bot.
|
|
2230
|
+
if (outcome !== "unavailable" && behavior === "answer" && message && cardMessage) {
|
|
2231
|
+
const settled = store.messagesFor(threadId).find((m) => m.id === cardMessage.id)?.card;
|
|
2232
|
+
if (settled) {
|
|
2233
|
+
store.patchMessage(threadId, cardMessage.id, { card: { ...settled, answeredText: message } });
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2188
2236
|
// The human's verdict, recorded only when it actually reached the engine:
|
|
2189
2237
|
// `unavailable` means the action never ran, and a "user-approved" row
|
|
2190
2238
|
// over a request nothing answered would be the audit log lying. A
|
|
@@ -2248,6 +2296,16 @@ function requestBehavior(value) {
|
|
|
2248
2296
|
// the last settled assistant text per thread, so a "finished" notification
|
|
2249
2297
|
// can carry what the bot actually said
|
|
2250
2298
|
const lastReply = new Map();
|
|
2299
|
+
/** the model each thread's provider session announced in session.started,
|
|
2300
|
+
* so a fallback notice can name the model Auto is unavailable for */
|
|
2301
|
+
const sessionModelByThread = new Map();
|
|
2302
|
+
/** threads already told that the provider's reviewer never started */
|
|
2303
|
+
const nativeReviewNoticed = new Set();
|
|
2304
|
+
/** a driver kind as the chat should name it: "claudeAgent" → "Claude" */
|
|
2305
|
+
const providerLabel = (provider) => {
|
|
2306
|
+
const bare = provider.replace(/Agent$/, "");
|
|
2307
|
+
return bare.charAt(0).toUpperCase() + bare.slice(1);
|
|
2308
|
+
};
|
|
2251
2309
|
/** Put a notification on the wire. Clients decide what to do with it — a
|
|
2252
2310
|
* desktop notification now, a push to a paired phone later. */
|
|
2253
2311
|
function notify(notification) {
|
|
@@ -2315,6 +2373,7 @@ const watchdog = new TurnWatchdog({
|
|
|
2315
2373
|
kind: "activity",
|
|
2316
2374
|
tool: { name: `error: no activity for ${minutes} minutes — the turn was stopped`, ok: false },
|
|
2317
2375
|
});
|
|
2376
|
+
settleDirectFollowup(stalledGeneration);
|
|
2318
2377
|
finalizeDelegationWatch(turn.threadId, false, "", "Delegated turn stalled and was stopped");
|
|
2319
2378
|
turnUsage.delete(turn.threadId);
|
|
2320
2379
|
roomStallCompletions.stall(turn.threadId);
|
|
@@ -2370,66 +2429,6 @@ const watchdog = new TurnWatchdog({
|
|
|
2370
2429
|
},
|
|
2371
2430
|
});
|
|
2372
2431
|
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
2432
|
bus.subscribe((event) => {
|
|
2434
2433
|
if (shouldIgnoreProviderEvent(event))
|
|
2435
2434
|
return;
|
|
@@ -2440,6 +2439,11 @@ bus.subscribe((event) => {
|
|
|
2440
2439
|
else if (event.type === "turn.completed") {
|
|
2441
2440
|
watchdog.settle(event.threadId);
|
|
2442
2441
|
revokeInternalCapabilityForProviderEvent(event);
|
|
2442
|
+
if (event.turnId) {
|
|
2443
|
+
const owner = directFollowupTurns.complete(event.threadId, event.turnId);
|
|
2444
|
+
if (owner)
|
|
2445
|
+
settleDirectFollowup(owner.generation);
|
|
2446
|
+
}
|
|
2443
2447
|
}
|
|
2444
2448
|
else if (event.type !== "session.exited")
|
|
2445
2449
|
watchdog.touch(event.threadId);
|
|
@@ -2835,6 +2839,8 @@ bus.subscribe((event) => {
|
|
|
2835
2839
|
if (bot && event.sessionId && event.providerInstanceId) {
|
|
2836
2840
|
store.setResumeCursor(bot.id, event.providerInstanceId, event.sessionId, event.threadId);
|
|
2837
2841
|
}
|
|
2842
|
+
if (typeof event.model === "string" && event.model)
|
|
2843
|
+
sessionModelByThread.set(event.threadId, event.model);
|
|
2838
2844
|
break;
|
|
2839
2845
|
case "item.completed":
|
|
2840
2846
|
if (event.itemType === "assistant_text") {
|
|
@@ -2913,29 +2919,42 @@ bus.subscribe((event) => {
|
|
|
2913
2919
|
}
|
|
2914
2920
|
break;
|
|
2915
2921
|
case "request.opened": {
|
|
2916
|
-
|
|
2917
|
-
//
|
|
2918
|
-
//
|
|
2919
|
-
//
|
|
2920
|
-
|
|
2922
|
+
// A structured ask carries the model's own options and has no
|
|
2923
|
+
// allow/deny answer, so it is a question no matter which channel the
|
|
2924
|
+
// provider routed it through — and, like every question, no approval
|
|
2925
|
+
// mode may ever answer it for the person.
|
|
2926
|
+
const permission = event.requestType === "permission" && !event.questions?.length;
|
|
2927
|
+
// A permission request here is one the provider left for a person: its
|
|
2928
|
+
// own mode already ran (Ask, Edits, Auto's reviewer, Custom's config).
|
|
2929
|
+
// OpenMausBot decides nothing about the action itself. Only Full access
|
|
2930
|
+
// answers, because that is exactly what the person granted. A QUESTION
|
|
2931
|
+
// always reaches the human — even Full access never invents an answer.
|
|
2921
2932
|
const asker = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
2922
2933
|
const unattended = permission && asker && event.requestId ? isUnattended(asker.id, event.threadId) : false;
|
|
2923
|
-
const effectiveApprovalMode = asker ? approvalModeForTurn(asker, isInternalTurn(event.threadId)
|
|
2934
|
+
const effectiveApprovalMode = asker ? approvalModeForTurn(asker, isInternalTurn(event.threadId)) : "ask";
|
|
2924
2935
|
const verdict = permission && asker && event.requestId
|
|
2925
|
-
? autoVerdict({
|
|
2926
|
-
// Use the same receiving-bot mode as the provider dispatch.
|
|
2927
|
-
approvalMode: effectiveApprovalMode,
|
|
2928
|
-
autoApprove: false,
|
|
2929
|
-
alwaysAllow: asker.alwaysAllow,
|
|
2930
|
-
}, event.tool, event.summary, {
|
|
2931
|
-
unattended,
|
|
2932
|
-
scope: event.approvalScope,
|
|
2933
|
-
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
2934
|
-
// Match the dispatched mode for every provider, including
|
|
2935
|
-
// delegated Custom and unattended Auto downgrades.
|
|
2936
|
-
nativeApproval: requiresNativeApproval(event.provider, effectiveApprovalMode),
|
|
2937
|
-
})
|
|
2936
|
+
? autoVerdict(effectiveApprovalMode, event.tool, { requiresExplicitApproval: event.requiresExplicitApproval })
|
|
2938
2937
|
: null;
|
|
2938
|
+
// Auto's reviewer is the engine's own. Claude accepts `--permission-mode
|
|
2939
|
+
// auto` for any model and starts in Manual without a word when auto is
|
|
2940
|
+
// unavailable (Haiku 4.5, Sonnet 4.5, an org that disabled it), so the
|
|
2941
|
+
// bot asks about everything. Say once per session why, and what stops it.
|
|
2942
|
+
if (permission &&
|
|
2943
|
+
asker &&
|
|
2944
|
+
event.nativeReview === "inactive" &&
|
|
2945
|
+
effectiveApprovalMode === "auto" &&
|
|
2946
|
+
!nativeReviewNoticed.has(event.threadId)) {
|
|
2947
|
+
nativeReviewNoticed.add(event.threadId);
|
|
2948
|
+
const model = sessionModelByThread.get(event.threadId);
|
|
2949
|
+
pushMessage({
|
|
2950
|
+
role: "bot",
|
|
2951
|
+
kind: "activity",
|
|
2952
|
+
tool: {
|
|
2953
|
+
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.`,
|
|
2954
|
+
ok: true,
|
|
2955
|
+
},
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2939
2958
|
if (verdict?.approve && asker && event.requestId) {
|
|
2940
2959
|
const settled = verdict.approve;
|
|
2941
2960
|
const instance = event.providerInstanceId
|
|
@@ -2971,7 +2990,6 @@ bus.subscribe((event) => {
|
|
|
2971
2990
|
summary,
|
|
2972
2991
|
decision: "auto-approved",
|
|
2973
2992
|
source: verdict.source,
|
|
2974
|
-
rule: verdict.rule,
|
|
2975
2993
|
});
|
|
2976
2994
|
}
|
|
2977
2995
|
catch {
|
|
@@ -2986,17 +3004,8 @@ bus.subscribe((event) => {
|
|
|
2986
3004
|
options: ["Allow", "Deny"],
|
|
2987
3005
|
requestId,
|
|
2988
3006
|
tool,
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
scope: event.approvalScope,
|
|
2992
|
-
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
2993
|
-
}),
|
|
2994
|
-
held: HELD_NOTE[verdict.source === "full-access"
|
|
2995
|
-
? "approval.held.undeliveredFull"
|
|
2996
|
-
: "approval.held.undelivered"],
|
|
2997
|
-
heldCode: verdict.source === "full-access"
|
|
2998
|
-
? "approval.held.undeliveredFull"
|
|
2999
|
-
: "approval.held.undelivered",
|
|
3007
|
+
held: HELD_NOTE["approval.held.undeliveredFull"],
|
|
3008
|
+
heldCode: "approval.held.undeliveredFull",
|
|
3000
3009
|
approvalScope: event.approvalScope,
|
|
3001
3010
|
},
|
|
3002
3011
|
});
|
|
@@ -3010,24 +3019,16 @@ bus.subscribe((event) => {
|
|
|
3010
3019
|
summary,
|
|
3011
3020
|
decision: "card-shown",
|
|
3012
3021
|
source: "auto-fallback",
|
|
3013
|
-
rule: verdict.rule,
|
|
3014
3022
|
});
|
|
3015
3023
|
}
|
|
3016
3024
|
})();
|
|
3017
3025
|
break;
|
|
3018
3026
|
}
|
|
3019
|
-
|
|
3020
|
-
//
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
3025
|
-
mode: asker ? approvalModeForOrigin(approvalModeFor(asker), { peerInitiated: isInternalTurn(event.threadId) }) : "ask",
|
|
3026
|
-
unattended: Boolean(unattended),
|
|
3027
|
-
fullAccessAvailable: asker && !isInternalTurn(event.threadId)
|
|
3028
|
-
? supportsApprovalMode(registry.cliTarget(asker.modelSelection.instanceId)?.driverKind, "full")
|
|
3029
|
-
: false,
|
|
3030
|
-
};
|
|
3027
|
+
const heldContext = { source: verdict?.source, permission };
|
|
3028
|
+
// A structured ask (Claude's AskUserQuestion) is a question whatever
|
|
3029
|
+
// the provider routed it as: it has no allow/deny answer, only the
|
|
3030
|
+
// model's own options. The card carries them so the person can choose.
|
|
3031
|
+
const questions = event.questions?.length ? event.questions : undefined;
|
|
3031
3032
|
const message = pushMessage({
|
|
3032
3033
|
role: "bot",
|
|
3033
3034
|
kind: "options",
|
|
@@ -3041,15 +3042,10 @@ bus.subscribe((event) => {
|
|
|
3041
3042
|
options: event.choices?.length ? event.choices : permission ? ["Allow", "Deny"] : [],
|
|
3042
3043
|
requestId: event.requestId,
|
|
3043
3044
|
tool: permission ? event.tool : undefined,
|
|
3044
|
-
|
|
3045
|
-
//
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
source: verdict?.source,
|
|
3049
|
-
scope: event.approvalScope,
|
|
3050
|
-
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
3051
|
-
})
|
|
3052
|
-
: undefined,
|
|
3045
|
+
questionRequest: questions ? { version: 1, questions } : undefined,
|
|
3046
|
+
// the provider can keep an allow for its session; the app keeps
|
|
3047
|
+
// no grant of its own for a provider's tool
|
|
3048
|
+
allowSession: permission && event.allowSession && !event.requiresExplicitApproval ? true : undefined,
|
|
3053
3049
|
// The text stays for cards saved before heldCode existed, and for
|
|
3054
3050
|
// clients that do not know the key yet.
|
|
3055
3051
|
held: approvalHeldReason(heldContext),
|
|
@@ -3059,37 +3055,8 @@ bus.subscribe((event) => {
|
|
|
3059
3055
|
});
|
|
3060
3056
|
if (event.requestId)
|
|
3061
3057
|
askMessageByRequest.set(`${event.threadId}:${event.requestId}`, message.id);
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
if (permission &&
|
|
3065
|
-
!event.requiresExplicitApproval &&
|
|
3066
|
-
asker &&
|
|
3067
|
-
event.requestId &&
|
|
3068
|
-
shouldReview({
|
|
3069
|
-
source: verdict?.source,
|
|
3070
|
-
mode: reviewMode,
|
|
3071
|
-
approvalMode: approvalModeFor(asker),
|
|
3072
|
-
unattended: Boolean(unattended),
|
|
3073
|
-
approvalScope: event.approvalScope,
|
|
3074
|
-
})) {
|
|
3075
|
-
// Review stays on the provider boundary that opened the request.
|
|
3076
|
-
// Falling back to an arbitrary sibling could disclose action details
|
|
3077
|
-
// to a provider the user did not choose for this bot.
|
|
3078
|
-
const instance = registry.get(event.providerInstanceId ?? asker.modelSelection.instanceId);
|
|
3079
|
-
if (instance?.reviewPermission) {
|
|
3080
|
-
reviewTask = reviewPermissionCard({
|
|
3081
|
-
instance,
|
|
3082
|
-
asker,
|
|
3083
|
-
threadId: event.threadId,
|
|
3084
|
-
requestId: event.requestId,
|
|
3085
|
-
messageId: message.id,
|
|
3086
|
-
tool: event.tool,
|
|
3087
|
-
summary: event.summary,
|
|
3088
|
-
});
|
|
3089
|
-
}
|
|
3090
|
-
}
|
|
3091
|
-
// Every card that reaches a human is a decision too — "a rule sent
|
|
3092
|
-
// this to you, and here is which one". `question` marks the cards no
|
|
3058
|
+
// Every card that reaches a human is a decision too: "the provider
|
|
3059
|
+
// left this for you, in this mode". `question` marks the cards no
|
|
3093
3060
|
// rule may ever answer; a permission card without a verdict (no known
|
|
3094
3061
|
// asker, or no requestId to answer through) can only mean nothing was
|
|
3095
3062
|
// granted.
|
|
@@ -3102,43 +3069,22 @@ bus.subscribe((event) => {
|
|
|
3102
3069
|
summary: event.summary,
|
|
3103
3070
|
decision: "card-shown",
|
|
3104
3071
|
source: !permission ? "question" : verdict ? verdict.source : "no-grant",
|
|
3105
|
-
rule: verdict?.rule,
|
|
3106
3072
|
unattended: unattended || undefined,
|
|
3107
3073
|
});
|
|
3108
3074
|
// Notify from HERE, not from a separate subscriber on request.opened:
|
|
3109
3075
|
// this is the branch where a card actually reached a human. Anything
|
|
3110
|
-
//
|
|
3111
|
-
|
|
3112
|
-
if (!asker)
|
|
3113
|
-
return;
|
|
3076
|
+
// Full access answered took the early return above and never buzzes.
|
|
3077
|
+
if (asker) {
|
|
3114
3078
|
const card = store.messagesFor(event.threadId).find((candidate) => candidate.id === message.id)?.card;
|
|
3115
|
-
if (
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
};
|
|
3125
|
-
if (reviewTask && reviewMode === "enforce") {
|
|
3126
|
-
// Avoid buzzing the owner for a card the reviewer is about to answer.
|
|
3127
|
-
// A deny, failure, or timeout falls back to the normal notification;
|
|
3128
|
-
// if the human already answered meanwhile, notifyHuman is a no-op.
|
|
3129
|
-
void reviewTask
|
|
3130
|
-
.catch(() => false)
|
|
3131
|
-
.then((approved) => {
|
|
3132
|
-
if (!approved)
|
|
3133
|
-
notifyHuman();
|
|
3134
|
-
});
|
|
3135
|
-
}
|
|
3136
|
-
else {
|
|
3137
|
-
// Watch mode notifies immediately, but its background audit must not
|
|
3138
|
-
// become an unhandled rejection if an unexpected store error occurs.
|
|
3139
|
-
if (reviewTask)
|
|
3140
|
-
void reviewTask.catch(() => false);
|
|
3141
|
-
notifyHuman();
|
|
3079
|
+
if (card && !card.answered) {
|
|
3080
|
+
// the bot is not working now — it is waiting on a person
|
|
3081
|
+
if (bot)
|
|
3082
|
+
store.setTaskActivity(bot.id, event.threadId, "waiting-on-you");
|
|
3083
|
+
else if (asker.busy)
|
|
3084
|
+
store.setActivity(asker.id, "waiting-on-you");
|
|
3085
|
+
const notificationBot = (routineRun && routineSourceOwner(routineRun)?.bot) || asker;
|
|
3086
|
+
notify(buildNotification(permission ? "approval" : "question", notificationBot, (routineRun && routineSourceThread(routineRun)) || event.threadId, event.summary));
|
|
3087
|
+
}
|
|
3142
3088
|
}
|
|
3143
3089
|
break;
|
|
3144
3090
|
}
|
|
@@ -3789,6 +3735,8 @@ bus.subscribe((event) => {
|
|
|
3789
3735
|
drainDelegationWakes();
|
|
3790
3736
|
});
|
|
3791
3737
|
function drainQueuedSends() {
|
|
3738
|
+
if (!followupsReady)
|
|
3739
|
+
return;
|
|
3792
3740
|
drainSteeredMessages(store, (botId, threadId, prompt, userMessage, excludeIds, unattended) =>
|
|
3793
3741
|
// A plain attended turn — no automationSource, no comms depth: exactly
|
|
3794
3742
|
// what typing the same words into an idle bot would run. The one
|
|
@@ -3797,15 +3745,19 @@ function drainQueuedSends() {
|
|
|
3797
3745
|
// Drain just appended the held lines; userMessage keeps startTurn
|
|
3798
3746
|
// from duplicating the last one, and excludeIds drops every drained
|
|
3799
3747
|
// line from the transcript-replay so they are not also in `prompt`.
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3748
|
+
new Promise((resolve, reject) => {
|
|
3749
|
+
void startTurn(botId, prompt, {
|
|
3750
|
+
threadId, userMessage, excludeMessageIds: excludeIds, unattended, onTurnSettled: resolve,
|
|
3751
|
+
}).catch((err) => {
|
|
3752
|
+
store.appendMessage(threadId, {
|
|
3753
|
+
role: "bot", kind: "activity",
|
|
3754
|
+
tool: {
|
|
3755
|
+
name: `error: queued message could not start — ${(err instanceof Error ? err.message : String(err)).slice(0, 120)}`,
|
|
3756
|
+
ok: false,
|
|
3757
|
+
},
|
|
3758
|
+
});
|
|
3759
|
+
resolve();
|
|
3760
|
+
}).catch(reject);
|
|
3809
3761
|
}),
|
|
3810
3762
|
// Provider completion can precede its dispatch promise: keep the queue
|
|
3811
3763
|
// intact until that exact handshake releases its runtime-only claim.
|
|
@@ -4210,6 +4162,8 @@ async function startTurn(botId, text, opts) {
|
|
|
4210
4162
|
const resourceOwner = { threadId, generation: dispatchClaimId };
|
|
4211
4163
|
turnResourceOwners.set(threadId, resourceOwner);
|
|
4212
4164
|
directTurnGenerationByThread.set(threadId, dispatchClaimId);
|
|
4165
|
+
if (opts?.onTurnSettled)
|
|
4166
|
+
directFollowupSettlers.set(dispatchClaimId, { threadId, settle: opts.onTurnSettled });
|
|
4213
4167
|
directTurnDispatchClaims.set(threadId, { id: dispatchClaimId, botId, threadId, phase: "setup" });
|
|
4214
4168
|
directTurnBots.set(threadId, bot);
|
|
4215
4169
|
beginInternalCapabilityGeneration(threadId, dispatchClaimId);
|
|
@@ -4602,6 +4556,7 @@ async function startTurn(botId, text, opts) {
|
|
|
4602
4556
|
// to a turn whose engine actually mounted them (setupMode is already
|
|
4603
4557
|
// false when they are not — see agentsMounted above)
|
|
4604
4558
|
{ id: "setup", label: "Setup", text: setupSystemPrompt(setupMode, { skills: skillAuthoring, cwd: liveBot?.cwd ?? bot.cwd }) },
|
|
4559
|
+
{ id: "files", label: "File locations", text: worksInWorkspace && opts?.runOn !== "cloud" ? workspaceLocationsPrompt(bot.id, cwd, liveBot?.cwd ?? bot.cwd) : "" },
|
|
4605
4560
|
{ id: "computer", label: "Computer", text: computerPrompt(computerPromptKind) },
|
|
4606
4561
|
{ id: "plan", label: "Surface", text: plan.note },
|
|
4607
4562
|
// gated on the integration, not the key: the hint only goes to a
|
|
@@ -4650,6 +4605,11 @@ async function startTurn(botId, text, opts) {
|
|
|
4650
4605
|
throw new DirectTurnSetupCancelled("turn stopped during provider setup");
|
|
4651
4606
|
}
|
|
4652
4607
|
bindInternalCapabilityToProviderTurn(threadId, dispatchClaimId, dispatch.value.turnId);
|
|
4608
|
+
if (directFollowupSettlers.has(dispatchClaimId) && dispatch.value.turnId &&
|
|
4609
|
+
!directFollowupTurns.bind(threadId, dispatchClaimId, dispatch.value.turnId)) {
|
|
4610
|
+
// This exact queued turn completed before its dispatch ACK arrived.
|
|
4611
|
+
settleDirectFollowup(dispatchClaimId);
|
|
4612
|
+
}
|
|
4653
4613
|
clearDirectTurnDispatch(threadId, dispatchClaimId);
|
|
4654
4614
|
// dispatched: the rewind is spent, and the old cursors are dead
|
|
4655
4615
|
if (rewound)
|
|
@@ -4683,6 +4643,7 @@ async function startTurn(botId, text, opts) {
|
|
|
4683
4643
|
}
|
|
4684
4644
|
}
|
|
4685
4645
|
catch (e) {
|
|
4646
|
+
settleDirectFollowup(dispatchClaimId);
|
|
4686
4647
|
clearCancelledProviderHandshake(threadId, `direct:${dispatchClaimId}`);
|
|
4687
4648
|
clearDirectTurnDispatch(threadId, dispatchClaimId);
|
|
4688
4649
|
revokeInternalCapabilityGeneration(threadId, dispatchClaimId);
|
|
@@ -5275,6 +5236,7 @@ const webhooks = new WebhookManager({
|
|
|
5275
5236
|
return !bot ? "missing" : bot.busy ? "busy" : "ready";
|
|
5276
5237
|
},
|
|
5277
5238
|
enqueue: (input) => routines.enqueueWebhook(input),
|
|
5239
|
+
findRun: (webhookId, deliveryId) => routines.webhookRunReceipt(webhookId, deliveryId),
|
|
5278
5240
|
cancelQueued: (webhookId, message) => routines.cancelQueuedWebhook(webhookId, message),
|
|
5279
5241
|
pendingRuns: (webhookId) => routines.activeWebhookRunCount(webhookId),
|
|
5280
5242
|
});
|
|
@@ -5386,7 +5348,7 @@ setupRetry = 0) {
|
|
|
5386
5348
|
}
|
|
5387
5349
|
revokeInternalCapabilitiesForThread(threadId);
|
|
5388
5350
|
spoken.add(botId);
|
|
5389
|
-
const preparedApprovalMode = approvalModeForTurn(bot, false
|
|
5351
|
+
const preparedApprovalMode = approvalModeForTurn(bot, false);
|
|
5390
5352
|
const preparedSelection = { ...bot.modelSelection };
|
|
5391
5353
|
const preparedComposio = bot.composio;
|
|
5392
5354
|
const instance = registry.get(bot.modelSelection.instanceId);
|
|
@@ -5509,7 +5471,7 @@ setupRetry = 0) {
|
|
|
5509
5471
|
if (!readyGroup || !stillOwnsThread || !readyGroup.memberIds.includes(readyBot.id))
|
|
5510
5472
|
return false;
|
|
5511
5473
|
const setupChanged = registry.get(preparedSelection.instanceId) !== instance ||
|
|
5512
|
-
approvalModeForTurn(readyBot, false
|
|
5474
|
+
approvalModeForTurn(readyBot, false) !== preparedApprovalMode ||
|
|
5513
5475
|
readyBot.modelSelection.instanceId !== preparedSelection.instanceId ||
|
|
5514
5476
|
readyBot.modelSelection.model !== preparedSelection.model ||
|
|
5515
5477
|
readyBot.modelSelection.effort !== preparedSelection.effort ||
|
|
@@ -5700,6 +5662,7 @@ setupRetry = 0) {
|
|
|
5700
5662
|
store.patchBot(bot.id, { soulDrift: drift.drift });
|
|
5701
5663
|
}
|
|
5702
5664
|
const roomSystem = buildSystemPrompt(system, store.bot(bot.id)?.soul ?? bot.soul ?? "", [
|
|
5665
|
+
{ id: "files", label: "File locations", text: workspace ? workspaceLocationsPrompt(bot.id, cwd, readyBot.cwd) : "" },
|
|
5703
5666
|
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {})) },
|
|
5704
5667
|
{ id: "computer", label: "Computer", text: computerPrompt(roomVmTarget ? localVmMode(cfg) === "per-bot" ? "vm-private" : "vm-shared" : null) },
|
|
5705
5668
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
@@ -5816,7 +5779,7 @@ setupRetry = 0) {
|
|
|
5816
5779
|
threadId,
|
|
5817
5780
|
text,
|
|
5818
5781
|
images: turnImages,
|
|
5819
|
-
approvalMode: approvalModeForTurn(readyBot, false
|
|
5782
|
+
approvalMode: approvalModeForTurn(readyBot, false),
|
|
5820
5783
|
system: roomSystem.text,
|
|
5821
5784
|
systemStable: roomSystem.stable,
|
|
5822
5785
|
systemVolatile: roomSystem.volatile,
|
|
@@ -6438,6 +6401,8 @@ function startGroupTurn(groupId, text, replyTo, sendId, channelMode = "chat", qu
|
|
|
6438
6401
|
return message;
|
|
6439
6402
|
}
|
|
6440
6403
|
function drainQueuedChannelSends() {
|
|
6404
|
+
if (!followupsReady)
|
|
6405
|
+
return;
|
|
6441
6406
|
drainChannelMessages((groupId) => {
|
|
6442
6407
|
const group = store.group(groupId);
|
|
6443
6408
|
return group ? groupIsWorking(group) : false;
|
|
@@ -6449,9 +6414,12 @@ function drainQueuedChannelSends() {
|
|
|
6449
6414
|
if (!group || !ownsThread)
|
|
6450
6415
|
return;
|
|
6451
6416
|
try {
|
|
6452
|
-
startGroupTurn(groupId, text, resolveReplyTarget(threadId, replyToId), sendId, mode, id, { via });
|
|
6417
|
+
startGroupTurn(groupId, text, resolveReplyTarget(threadId, replyToId), sendId, mode, id, { via, threadId });
|
|
6453
6418
|
}
|
|
6454
6419
|
catch (error) {
|
|
6420
|
+
if (!store.messagesFor(threadId).some((message) => message.queueId === id && message.role === "user")) {
|
|
6421
|
+
store.appendMessage(threadId, { role: "user", kind: "text", text, replyToId, sendId, channelMode: mode, queueId: id, via });
|
|
6422
|
+
}
|
|
6455
6423
|
store.appendMessage(threadId, {
|
|
6456
6424
|
role: "bot",
|
|
6457
6425
|
kind: "activity",
|
|
@@ -6464,6 +6432,7 @@ function drainQueuedChannelSends() {
|
|
|
6464
6432
|
// A message with no eligible responder creates no operation. Continue
|
|
6465
6433
|
// draining instead of leaving later user messages behind it forever.
|
|
6466
6434
|
queueMicrotask(drainQueuedChannelSends);
|
|
6435
|
+
return groupQueues.get(groupId);
|
|
6467
6436
|
});
|
|
6468
6437
|
}
|
|
6469
6438
|
function sameCalendarRoster(group, botIds) {
|
|
@@ -7371,6 +7340,14 @@ function configStatus() {
|
|
|
7371
7340
|
showToolCalls: showToolCallsEnabled(cfg),
|
|
7372
7341
|
browser: builtInBrowserEnabled(cfg),
|
|
7373
7342
|
},
|
|
7343
|
+
// first-run progress — not a secret; the app decides whether to show
|
|
7344
|
+
// the welcome tour from this, never from browser storage
|
|
7345
|
+
onboarding: {
|
|
7346
|
+
completedAt: cfg.onboarding?.completedAt ?? "",
|
|
7347
|
+
version: cfg.onboarding?.version ?? 0,
|
|
7348
|
+
reelSeen: cfg.onboarding?.reelSeen === true,
|
|
7349
|
+
hintsSeen: cfg.onboarding?.hintsSeen ?? [],
|
|
7350
|
+
},
|
|
7374
7351
|
// Which browser this server can give bots: the desktop app's surface,
|
|
7375
7352
|
// the agent-browser engine, or nothing yet (with the reason).
|
|
7376
7353
|
browserEngine: browserEngineSummary(),
|
|
@@ -7489,6 +7466,7 @@ async function reloadProviders() {
|
|
|
7489
7466
|
});
|
|
7490
7467
|
store.setTaskActivity(botId, threadId, "idle");
|
|
7491
7468
|
}
|
|
7469
|
+
settleDirectFollowup(owner?.generation);
|
|
7492
7470
|
retryDelegationsWaitingOn(botId);
|
|
7493
7471
|
}
|
|
7494
7472
|
for (const [threadId, speaker] of rooms) {
|
|
@@ -10506,6 +10484,9 @@ const handleRequest = async (req, res) => {
|
|
|
10506
10484
|
}
|
|
10507
10485
|
const receipt = await sendSequencer.run(sendId ? `group:${group.id}:${threadId}:${sendId}` : undefined, sendFingerprint(text, replyTo?.id, channelMode), async () => {
|
|
10508
10486
|
if (sendId) {
|
|
10487
|
+
if (cancelledChatFollowup("channel", group.id, threadId, sendId)) {
|
|
10488
|
+
throw Object.assign(new Error("this queued sendId was cancelled; send a new message to try again"), { status: 409 });
|
|
10489
|
+
}
|
|
10509
10490
|
const accepted = acceptedSendMatch(store.messagesFor(threadId), sendId, text, replyTo?.id, channelMode);
|
|
10510
10491
|
if (accepted.kind === "conflict") {
|
|
10511
10492
|
throw Object.assign(new Error("sendId already belongs to another message"), { status: 409 });
|
|
@@ -11101,12 +11082,6 @@ const handleRequest = async (req, res) => {
|
|
|
11101
11082
|
error: "This approval-level change can only be made from the packaged desktop app",
|
|
11102
11083
|
});
|
|
11103
11084
|
}
|
|
11104
|
-
if (body.autoReview !== undefined) {
|
|
11105
|
-
if (body.autoReview !== "off" && body.autoReview !== "shadow" && body.autoReview !== "enforce") {
|
|
11106
|
-
return json(res, 400, { error: "autoReview must be off, shadow, or enforce" });
|
|
11107
|
-
}
|
|
11108
|
-
patch.autoReview = body.autoReview;
|
|
11109
|
-
}
|
|
11110
11085
|
// "Auto on this Mac" hands a bot the user's real session, so the grant
|
|
11111
11086
|
// must prove a human saw the warning. The desktop dialog is the only
|
|
11112
11087
|
// caller that sends acknowledgeLocalAuto; without it a PATCH that would
|
|
@@ -11462,6 +11437,7 @@ const handleRequest = async (req, res) => {
|
|
|
11462
11437
|
// staged provider images into a message, so dispose them here.
|
|
11463
11438
|
for (const task of store.tasks(bot.id)) {
|
|
11464
11439
|
purgeGeneratedImagesForThread(task.threadId);
|
|
11440
|
+
settleDirectFollowup(directTurnGenerationByThread.get(task.threadId));
|
|
11465
11441
|
directTurnGenerationByThread.delete(task.threadId);
|
|
11466
11442
|
directTurnBots.delete(task.threadId);
|
|
11467
11443
|
}
|
|
@@ -12005,6 +11981,9 @@ const handleRequest = async (req, res) => {
|
|
|
12005
11981
|
const replyTo = resolveReplyTarget(threadId, body.replyToId);
|
|
12006
11982
|
const receipt = await sendSequencer.run(sendId ? `bot:${bot.id}:${threadId}:${sendId}` : undefined, sendFingerprint(text, replyTo?.id), async () => {
|
|
12007
11983
|
if (sendId) {
|
|
11984
|
+
if (cancelledChatFollowup("bot", bot.id, threadId, sendId)) {
|
|
11985
|
+
throw Object.assign(new Error("this queued sendId was cancelled; send a new message to try again"), { status: 409 });
|
|
11986
|
+
}
|
|
12008
11987
|
const accepted = acceptedSendMatch(store.messagesFor(threadId), sendId, text, replyTo?.id);
|
|
12009
11988
|
if (accepted.kind === "conflict") {
|
|
12010
11989
|
throw Object.assign(new Error("sendId already belongs to another message"), { status: 409 });
|
|
@@ -12204,7 +12183,7 @@ const handleRequest = async (req, res) => {
|
|
|
12204
12183
|
resolvePeerComms(approvalBus, String(body.requestId), behavior)) {
|
|
12205
12184
|
return json(res, 200, { ok: true, outcome: behavior === "allow" ? "allowed-once" : "rejected" });
|
|
12206
12185
|
}
|
|
12207
|
-
const outcome = await answerRequest(bot.threadId, bot.modelSelection.instanceId, String(body.requestId), behavior, body.message, { id: bot.id, name: bot.name });
|
|
12186
|
+
const outcome = await answerRequest(bot.threadId, bot.modelSelection.instanceId, String(body.requestId), behavior, body.message, { id: bot.id, name: bot.name }, body.always === true);
|
|
12208
12187
|
return json(res, 200, { ok: true, outcome });
|
|
12209
12188
|
}
|
|
12210
12189
|
// Answer by THREAD, so a request raised inside a room can be answered
|
|
@@ -12289,7 +12268,7 @@ const handleRequest = async (req, res) => {
|
|
|
12289
12268
|
if (!owner && !pending)
|
|
12290
12269
|
return json(res, 404, { error: "nothing is waiting on an answer in this conversation" });
|
|
12291
12270
|
const requestOwner = owner ? botForThread(owner.id, threadId) : null;
|
|
12292
|
-
const outcome = await answerRequest(threadId, requestOwner?.modelSelection.instanceId ?? "", requestId, behavior, body.message, owner ? { id: owner.id, name: owner.name } : undefined);
|
|
12271
|
+
const outcome = await answerRequest(threadId, requestOwner?.modelSelection.instanceId ?? "", requestId, behavior, body.message, owner ? { id: owner.id, name: owner.name } : undefined, body.always === true);
|
|
12293
12272
|
return json(res, 200, { ok: true, outcome });
|
|
12294
12273
|
}
|
|
12295
12274
|
m = path.match(/^\/api\/bots\/([\w-]+)\/interrupt$/);
|
|
@@ -12460,15 +12439,17 @@ const handleRequest = async (req, res) => {
|
|
|
12460
12439
|
const current = store.projectBotForTask(m[1], m[2]);
|
|
12461
12440
|
if (!current)
|
|
12462
12441
|
return json(res, 404, { error: "no such task" });
|
|
12463
|
-
const allowed = new Set(["title", "projectId", "modelSelection", "approvalMode", "autoApprove", "requireAvailableModel", "pinnedMessageId", "acknowledgeLocalAuto"]);
|
|
12442
|
+
const allowed = new Set(["title", "projectId", "modelSelection", "updateBotDefault", "approvalMode", "autoApprove", "requireAvailableModel", "pinnedMessageId", "acknowledgeLocalAuto"]);
|
|
12464
12443
|
if (Object.keys(body).some((key) => !allowed.has(key)))
|
|
12465
12444
|
return json(res, 400, { error: "unsupported thread setting" });
|
|
12466
|
-
for (const key of ["requireAvailableModel", "acknowledgeLocalAuto"]) {
|
|
12445
|
+
for (const key of ["requireAvailableModel", "acknowledgeLocalAuto", "updateBotDefault"]) {
|
|
12467
12446
|
if (body[key] !== undefined && typeof body[key] !== "boolean")
|
|
12468
12447
|
return json(res, 400, { error: `${key} must be a boolean` });
|
|
12469
12448
|
}
|
|
12470
12449
|
if (body.requireAvailableModel === true && body.modelSelection === undefined)
|
|
12471
12450
|
return json(res, 400, { error: "requireAvailableModel requires modelSelection" });
|
|
12451
|
+
if (body.updateBotDefault === true && body.modelSelection === undefined)
|
|
12452
|
+
return json(res, 400, { error: "updateBotDefault requires modelSelection" });
|
|
12472
12453
|
const patch = {};
|
|
12473
12454
|
if (body.projectId !== undefined) {
|
|
12474
12455
|
if (body.projectId === null)
|
|
@@ -12505,8 +12486,11 @@ const handleRequest = async (req, res) => {
|
|
|
12505
12486
|
const mode = body.approvalMode ?? (body.autoApprove ? "auto" : "ask");
|
|
12506
12487
|
// Elevated modes still require the trusted desktop transition. A
|
|
12507
12488
|
// thread settings PATCH cannot manufacture that grant.
|
|
12508
|
-
if (mode !== "ask" && mode !== "auto")
|
|
12509
|
-
return json(res, 403, { error: "Full and Custom access require
|
|
12489
|
+
if (mode !== "ask" && mode !== "auto" && mode !== "edits")
|
|
12490
|
+
return json(res, 403, { error: "Full and Custom access require trusted desktop confirmation" });
|
|
12491
|
+
if (!supportsApprovalMode(registry.cliTarget(current.modelSelection.instanceId)?.driverKind, mode)) {
|
|
12492
|
+
return json(res, 400, { error: "This provider does not support the selected approval level" });
|
|
12493
|
+
}
|
|
12510
12494
|
if (threadBusy(current.id, current.threadId))
|
|
12511
12495
|
return json(res, 409, { error: "stop this thread before changing its approval mode" });
|
|
12512
12496
|
if (current.approvalGrant)
|
|
@@ -12526,6 +12510,24 @@ const handleRequest = async (req, res) => {
|
|
|
12526
12510
|
registry.cliTarget(patch.modelSelection.instanceId)?.driverKind !== registry.cliTarget(current.modelSelection.instanceId)?.driverKind)) {
|
|
12527
12511
|
return json(res, 400, { error: "Choose Ask for this thread before changing providers with elevated permissions" });
|
|
12528
12512
|
}
|
|
12513
|
+
if (body.updateBotDefault === true && patch.modelSelection) {
|
|
12514
|
+
const profile = store.bot(current.id);
|
|
12515
|
+
const checked = checkedModelSelection(patch.modelSelection, {
|
|
12516
|
+
selection: profile.modelSelection, busy: Boolean(activeGroupTurnForBot(current.id)),
|
|
12517
|
+
});
|
|
12518
|
+
if (!checked.ok)
|
|
12519
|
+
return json(res, checked.status, { error: checked.error });
|
|
12520
|
+
const defaultMode = approvalModeFor(profile);
|
|
12521
|
+
if ((defaultMode === "full" || defaultMode === "custom") &&
|
|
12522
|
+
(!supportsApprovalMode(registry.cliTarget(patch.modelSelection.instanceId)?.driverKind, defaultMode) ||
|
|
12523
|
+
registry.cliTarget(patch.modelSelection.instanceId)?.driverKind !== registry.cliTarget(profile.modelSelection.instanceId)?.driverKind)) {
|
|
12524
|
+
return json(res, 400, { error: "Choose Ask in bot settings before changing its default provider with elevated permissions" });
|
|
12525
|
+
}
|
|
12526
|
+
// Validate both scopes before saving either. Existing sibling threads
|
|
12527
|
+
// retain their selections; only this pinned thread and future/group
|
|
12528
|
+
// turns adopt the new default. Do not target the currently selected tab.
|
|
12529
|
+
store.patchBot(current.id, { modelSelection: patch.modelSelection });
|
|
12530
|
+
}
|
|
12529
12531
|
const task = store.patchTask(m[1], m[2], patch);
|
|
12530
12532
|
const fresh = botWithThread(store.bot(m[1]));
|
|
12531
12533
|
broadcast({ kind: "bot", bot: fresh });
|
|
@@ -12546,6 +12548,7 @@ const handleRequest = async (req, res) => {
|
|
|
12546
12548
|
const updated = store.deleteTask(m[1], m[2]);
|
|
12547
12549
|
if (!updated)
|
|
12548
12550
|
return json(res, 400, { error: "a bot keeps at least one task" });
|
|
12551
|
+
settleDirectFollowup(directTurnGenerationByThread.get(m[2]));
|
|
12549
12552
|
rejectDeletedThreadSkillStages(stagedSkillCleanups);
|
|
12550
12553
|
const fresh = botWithThread(updated);
|
|
12551
12554
|
broadcast({ kind: "bot", bot: fresh });
|
|
@@ -13641,19 +13644,10 @@ const handleRequest = async (req, res) => {
|
|
|
13641
13644
|
browserReferenceCleanupError = error;
|
|
13642
13645
|
}
|
|
13643
13646
|
}
|
|
13644
|
-
// Provider keys change the fleet. Profile, language, voice, VPS,
|
|
13645
|
-
//
|
|
13646
|
-
// should not interrupt in-flight turns.
|
|
13647
|
-
const reloadKeys =
|
|
13648
|
-
key !== "language" &&
|
|
13649
|
-
key !== "tts" &&
|
|
13650
|
-
key !== "imageGen" &&
|
|
13651
|
-
key !== "vps" &&
|
|
13652
|
-
key !== "rooms" &&
|
|
13653
|
-
key !== "threads" &&
|
|
13654
|
-
key !== "localVm" &&
|
|
13655
|
-
key !== "features" &&
|
|
13656
|
-
key !== "browserProfiles");
|
|
13647
|
+
// Provider keys change the fleet. Profile, language, voice, VPS, room
|
|
13648
|
+
// timeout, and onboarding progress changes do not rebuild it: no driver
|
|
13649
|
+
// reads them, and they should not interrupt in-flight turns.
|
|
13650
|
+
const reloadKeys = providerReloadKeys(patch);
|
|
13657
13651
|
// Config is already durable. A provider credential or runtime change
|
|
13658
13652
|
// invalidates every old child immediately, including when browser
|
|
13659
13653
|
// cleanup below has to await Electron before reloadProviders begins.
|
|
@@ -14149,8 +14143,45 @@ try {
|
|
|
14149
14143
|
catch (error) {
|
|
14150
14144
|
console.warn(`attachments: startup partial cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
14151
14145
|
}
|
|
14146
|
+
// A dispatch claim is deliberately committed before transcript/provider work.
|
|
14147
|
+
// If we died after that point, its outcome is unknown: recover the user's words
|
|
14148
|
+
// and a review notice, never hand them to a model for a second execution.
|
|
14149
|
+
for (const row of chatFollowups()) {
|
|
14150
|
+
if (row.status !== "dispatching" && row.status !== "interrupted")
|
|
14151
|
+
continue;
|
|
14152
|
+
const owned = row.kind === "bot"
|
|
14153
|
+
? Boolean(store.taskByThread(row.ownerId, row.threadId))
|
|
14154
|
+
: Boolean(store.groupByThread(row.threadId)?.id === row.ownerId);
|
|
14155
|
+
if (!owned) {
|
|
14156
|
+
settleChatFollowups([row.id], "cancelled");
|
|
14157
|
+
continue;
|
|
14158
|
+
}
|
|
14159
|
+
settleChatFollowups([row.id], "interrupted");
|
|
14160
|
+
const messages = store.messagesFor(row.threadId);
|
|
14161
|
+
if (!messages.some((message) => message.queueId === row.id && message.role === "user")) {
|
|
14162
|
+
store.appendMessage(row.threadId, {
|
|
14163
|
+
role: "user", kind: "text", text: row.payload.text, replyToId: row.payload.replyToId,
|
|
14164
|
+
sendId: row.payload.sendId, queueId: row.id,
|
|
14165
|
+
...(row.kind === "channel" ? { channelMode: row.payload.mode, via: row.payload.via } : {}),
|
|
14166
|
+
});
|
|
14167
|
+
}
|
|
14168
|
+
if (!messages.some((message) => message.queueId === row.id && message.kind === "activity")) {
|
|
14169
|
+
store.appendMessage(row.threadId, {
|
|
14170
|
+
role: "bot", kind: "activity", queueId: row.id,
|
|
14171
|
+
tool: { name: "Queued follow-up interrupted by restart or restore — it may have already run. Review the result before sending it again.", ok: false },
|
|
14172
|
+
});
|
|
14173
|
+
}
|
|
14174
|
+
// The FULL-sync retirement also flushes both transcript writes. Retrying
|
|
14175
|
+
// this sendId now finds the canonical message, without a permanent journal scan.
|
|
14176
|
+
settleChatFollowups([row.id], null);
|
|
14177
|
+
}
|
|
14178
|
+
restoreSteeredMessages();
|
|
14179
|
+
restoreChannelMessages();
|
|
14152
14180
|
server.listen(PORT, "127.0.0.1", () => {
|
|
14153
14181
|
console.log(`openmausbot server on http://127.0.0.1:${PORT}`);
|
|
14182
|
+
followupsReady = true;
|
|
14183
|
+
drainQueuedSends();
|
|
14184
|
+
drainQueuedChannelSends();
|
|
14154
14185
|
// Startup work uses the same turn dispatcher and local tool endpoint as
|
|
14155
14186
|
// ordinary chat. Start only once every registry is initialized and the
|
|
14156
14187
|
// endpoint is listening; earlier dispatch can hit uninitialized bindings.
|
|
@@ -14188,6 +14219,7 @@ if (TUNNEL_SOCKET) {
|
|
|
14188
14219
|
const gracefulShutdown = createGracefulShutdown({
|
|
14189
14220
|
cleanup: [
|
|
14190
14221
|
() => {
|
|
14222
|
+
followupsReady = false;
|
|
14191
14223
|
// Child MCP processes and the HTTP listener can remain alive while the
|
|
14192
14224
|
// asynchronous shutdown jobs drain. Invalidate their turn bearers before
|
|
14193
14225
|
// any cleanup function reaches an await.
|