openmausbot 0.1.73 → 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-BYbK7ber.js → index-CwoQdM0z.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +9 -0
- package/dist-server/index.js +592 -453
- package/dist-server/mcp-gate.js +58 -16
- package/dist-server/openmausbot.js +186 -64
- package/dist-server/pair-cli.js +186 -64
- 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/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 +22 -11
- package/dist-server/server/engine-install.js +9 -5
- package/dist-server/server/index.js +185 -223
- 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/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-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";
|
|
@@ -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);
|
|
@@ -2835,6 +2803,8 @@ bus.subscribe((event) => {
|
|
|
2835
2803
|
if (bot && event.sessionId && event.providerInstanceId) {
|
|
2836
2804
|
store.setResumeCursor(bot.id, event.providerInstanceId, event.sessionId, event.threadId);
|
|
2837
2805
|
}
|
|
2806
|
+
if (typeof event.model === "string" && event.model)
|
|
2807
|
+
sessionModelByThread.set(event.threadId, event.model);
|
|
2838
2808
|
break;
|
|
2839
2809
|
case "item.completed":
|
|
2840
2810
|
if (event.itemType === "assistant_text") {
|
|
@@ -2914,28 +2884,37 @@ bus.subscribe((event) => {
|
|
|
2914
2884
|
break;
|
|
2915
2885
|
case "request.opened": {
|
|
2916
2886
|
const permission = event.requestType === "permission";
|
|
2917
|
-
//
|
|
2918
|
-
//
|
|
2919
|
-
//
|
|
2920
|
-
//
|
|
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.
|
|
2921
2892
|
const asker = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
2922
2893
|
const unattended = permission && asker && event.requestId ? isUnattended(asker.id, event.threadId) : false;
|
|
2923
|
-
const effectiveApprovalMode = asker ? approvalModeForTurn(asker, isInternalTurn(event.threadId)
|
|
2894
|
+
const effectiveApprovalMode = asker ? approvalModeForTurn(asker, isInternalTurn(event.threadId)) : "ask";
|
|
2924
2895
|
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
|
-
})
|
|
2896
|
+
? autoVerdict(effectiveApprovalMode, event.tool, { requiresExplicitApproval: event.requiresExplicitApproval })
|
|
2938
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
|
+
}
|
|
2939
2918
|
if (verdict?.approve && asker && event.requestId) {
|
|
2940
2919
|
const settled = verdict.approve;
|
|
2941
2920
|
const instance = event.providerInstanceId
|
|
@@ -2971,7 +2950,6 @@ bus.subscribe((event) => {
|
|
|
2971
2950
|
summary,
|
|
2972
2951
|
decision: "auto-approved",
|
|
2973
2952
|
source: verdict.source,
|
|
2974
|
-
rule: verdict.rule,
|
|
2975
2953
|
});
|
|
2976
2954
|
}
|
|
2977
2955
|
catch {
|
|
@@ -2986,17 +2964,8 @@ bus.subscribe((event) => {
|
|
|
2986
2964
|
options: ["Allow", "Deny"],
|
|
2987
2965
|
requestId,
|
|
2988
2966
|
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",
|
|
2967
|
+
held: HELD_NOTE["approval.held.undeliveredFull"],
|
|
2968
|
+
heldCode: "approval.held.undeliveredFull",
|
|
3000
2969
|
approvalScope: event.approvalScope,
|
|
3001
2970
|
},
|
|
3002
2971
|
});
|
|
@@ -3010,24 +2979,12 @@ bus.subscribe((event) => {
|
|
|
3010
2979
|
summary,
|
|
3011
2980
|
decision: "card-shown",
|
|
3012
2981
|
source: "auto-fallback",
|
|
3013
|
-
rule: verdict.rule,
|
|
3014
2982
|
});
|
|
3015
2983
|
}
|
|
3016
2984
|
})();
|
|
3017
2985
|
break;
|
|
3018
2986
|
}
|
|
3019
|
-
|
|
3020
|
-
// no mode to explain, but the sandbox note still applies.
|
|
3021
|
-
const heldContext = {
|
|
3022
|
-
source: verdict?.source,
|
|
3023
|
-
permission,
|
|
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
|
-
};
|
|
2987
|
+
const heldContext = { source: verdict?.source, permission };
|
|
3031
2988
|
const message = pushMessage({
|
|
3032
2989
|
role: "bot",
|
|
3033
2990
|
kind: "options",
|
|
@@ -3041,15 +2998,9 @@ bus.subscribe((event) => {
|
|
|
3041
2998
|
options: event.choices?.length ? event.choices : permission ? ["Allow", "Deny"] : [],
|
|
3042
2999
|
requestId: event.requestId,
|
|
3043
3000
|
tool: permission ? event.tool : undefined,
|
|
3044
|
-
// the
|
|
3045
|
-
//
|
|
3046
|
-
|
|
3047
|
-
? rememberableApprovalKey(asker, event.tool, event.summary, {
|
|
3048
|
-
source: verdict?.source,
|
|
3049
|
-
scope: event.approvalScope,
|
|
3050
|
-
requiresExplicitApproval: event.requiresExplicitApproval,
|
|
3051
|
-
})
|
|
3052
|
-
: 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,
|
|
3053
3004
|
// The text stays for cards saved before heldCode existed, and for
|
|
3054
3005
|
// clients that do not know the key yet.
|
|
3055
3006
|
held: approvalHeldReason(heldContext),
|
|
@@ -3059,37 +3010,8 @@ bus.subscribe((event) => {
|
|
|
3059
3010
|
});
|
|
3060
3011
|
if (event.requestId)
|
|
3061
3012
|
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
|
|
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
|
|
3093
3015
|
// rule may ever answer; a permission card without a verdict (no known
|
|
3094
3016
|
// asker, or no requestId to answer through) can only mean nothing was
|
|
3095
3017
|
// granted.
|
|
@@ -3102,43 +3024,22 @@ bus.subscribe((event) => {
|
|
|
3102
3024
|
summary: event.summary,
|
|
3103
3025
|
decision: "card-shown",
|
|
3104
3026
|
source: !permission ? "question" : verdict ? verdict.source : "no-grant",
|
|
3105
|
-
rule: verdict?.rule,
|
|
3106
3027
|
unattended: unattended || undefined,
|
|
3107
3028
|
});
|
|
3108
3029
|
// Notify from HERE, not from a separate subscriber on request.opened:
|
|
3109
3030
|
// this is the branch where a card actually reached a human. Anything
|
|
3110
|
-
//
|
|
3111
|
-
|
|
3112
|
-
if (!asker)
|
|
3113
|
-
return;
|
|
3031
|
+
// Full access answered took the early return above and never buzzes.
|
|
3032
|
+
if (asker) {
|
|
3114
3033
|
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();
|
|
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
|
+
}
|
|
3142
3043
|
}
|
|
3143
3044
|
break;
|
|
3144
3045
|
}
|
|
@@ -3789,6 +3690,8 @@ bus.subscribe((event) => {
|
|
|
3789
3690
|
drainDelegationWakes();
|
|
3790
3691
|
});
|
|
3791
3692
|
function drainQueuedSends() {
|
|
3693
|
+
if (!followupsReady)
|
|
3694
|
+
return;
|
|
3792
3695
|
drainSteeredMessages(store, (botId, threadId, prompt, userMessage, excludeIds, unattended) =>
|
|
3793
3696
|
// A plain attended turn — no automationSource, no comms depth: exactly
|
|
3794
3697
|
// what typing the same words into an idle bot would run. The one
|
|
@@ -3797,15 +3700,19 @@ function drainQueuedSends() {
|
|
|
3797
3700
|
// Drain just appended the held lines; userMessage keeps startTurn
|
|
3798
3701
|
// from duplicating the last one, and excludeIds drops every drained
|
|
3799
3702
|
// line from the transcript-replay so they are not also in `prompt`.
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
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);
|
|
3809
3716
|
}),
|
|
3810
3717
|
// Provider completion can precede its dispatch promise: keep the queue
|
|
3811
3718
|
// intact until that exact handshake releases its runtime-only claim.
|
|
@@ -4210,6 +4117,8 @@ async function startTurn(botId, text, opts) {
|
|
|
4210
4117
|
const resourceOwner = { threadId, generation: dispatchClaimId };
|
|
4211
4118
|
turnResourceOwners.set(threadId, resourceOwner);
|
|
4212
4119
|
directTurnGenerationByThread.set(threadId, dispatchClaimId);
|
|
4120
|
+
if (opts?.onTurnSettled)
|
|
4121
|
+
directFollowupSettlers.set(dispatchClaimId, { threadId, settle: opts.onTurnSettled });
|
|
4213
4122
|
directTurnDispatchClaims.set(threadId, { id: dispatchClaimId, botId, threadId, phase: "setup" });
|
|
4214
4123
|
directTurnBots.set(threadId, bot);
|
|
4215
4124
|
beginInternalCapabilityGeneration(threadId, dispatchClaimId);
|
|
@@ -4650,6 +4559,11 @@ async function startTurn(botId, text, opts) {
|
|
|
4650
4559
|
throw new DirectTurnSetupCancelled("turn stopped during provider setup");
|
|
4651
4560
|
}
|
|
4652
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
|
+
}
|
|
4653
4567
|
clearDirectTurnDispatch(threadId, dispatchClaimId);
|
|
4654
4568
|
// dispatched: the rewind is spent, and the old cursors are dead
|
|
4655
4569
|
if (rewound)
|
|
@@ -4683,6 +4597,7 @@ async function startTurn(botId, text, opts) {
|
|
|
4683
4597
|
}
|
|
4684
4598
|
}
|
|
4685
4599
|
catch (e) {
|
|
4600
|
+
settleDirectFollowup(dispatchClaimId);
|
|
4686
4601
|
clearCancelledProviderHandshake(threadId, `direct:${dispatchClaimId}`);
|
|
4687
4602
|
clearDirectTurnDispatch(threadId, dispatchClaimId);
|
|
4688
4603
|
revokeInternalCapabilityGeneration(threadId, dispatchClaimId);
|
|
@@ -5275,6 +5190,7 @@ const webhooks = new WebhookManager({
|
|
|
5275
5190
|
return !bot ? "missing" : bot.busy ? "busy" : "ready";
|
|
5276
5191
|
},
|
|
5277
5192
|
enqueue: (input) => routines.enqueueWebhook(input),
|
|
5193
|
+
findRun: (webhookId, deliveryId) => routines.webhookRunReceipt(webhookId, deliveryId),
|
|
5278
5194
|
cancelQueued: (webhookId, message) => routines.cancelQueuedWebhook(webhookId, message),
|
|
5279
5195
|
pendingRuns: (webhookId) => routines.activeWebhookRunCount(webhookId),
|
|
5280
5196
|
});
|
|
@@ -5386,7 +5302,7 @@ setupRetry = 0) {
|
|
|
5386
5302
|
}
|
|
5387
5303
|
revokeInternalCapabilitiesForThread(threadId);
|
|
5388
5304
|
spoken.add(botId);
|
|
5389
|
-
const preparedApprovalMode = approvalModeForTurn(bot, false
|
|
5305
|
+
const preparedApprovalMode = approvalModeForTurn(bot, false);
|
|
5390
5306
|
const preparedSelection = { ...bot.modelSelection };
|
|
5391
5307
|
const preparedComposio = bot.composio;
|
|
5392
5308
|
const instance = registry.get(bot.modelSelection.instanceId);
|
|
@@ -5509,7 +5425,7 @@ setupRetry = 0) {
|
|
|
5509
5425
|
if (!readyGroup || !stillOwnsThread || !readyGroup.memberIds.includes(readyBot.id))
|
|
5510
5426
|
return false;
|
|
5511
5427
|
const setupChanged = registry.get(preparedSelection.instanceId) !== instance ||
|
|
5512
|
-
approvalModeForTurn(readyBot, false
|
|
5428
|
+
approvalModeForTurn(readyBot, false) !== preparedApprovalMode ||
|
|
5513
5429
|
readyBot.modelSelection.instanceId !== preparedSelection.instanceId ||
|
|
5514
5430
|
readyBot.modelSelection.model !== preparedSelection.model ||
|
|
5515
5431
|
readyBot.modelSelection.effort !== preparedSelection.effort ||
|
|
@@ -5816,7 +5732,7 @@ setupRetry = 0) {
|
|
|
5816
5732
|
threadId,
|
|
5817
5733
|
text,
|
|
5818
5734
|
images: turnImages,
|
|
5819
|
-
approvalMode: approvalModeForTurn(readyBot, false
|
|
5735
|
+
approvalMode: approvalModeForTurn(readyBot, false),
|
|
5820
5736
|
system: roomSystem.text,
|
|
5821
5737
|
systemStable: roomSystem.stable,
|
|
5822
5738
|
systemVolatile: roomSystem.volatile,
|
|
@@ -6438,6 +6354,8 @@ function startGroupTurn(groupId, text, replyTo, sendId, channelMode = "chat", qu
|
|
|
6438
6354
|
return message;
|
|
6439
6355
|
}
|
|
6440
6356
|
function drainQueuedChannelSends() {
|
|
6357
|
+
if (!followupsReady)
|
|
6358
|
+
return;
|
|
6441
6359
|
drainChannelMessages((groupId) => {
|
|
6442
6360
|
const group = store.group(groupId);
|
|
6443
6361
|
return group ? groupIsWorking(group) : false;
|
|
@@ -6449,9 +6367,12 @@ function drainQueuedChannelSends() {
|
|
|
6449
6367
|
if (!group || !ownsThread)
|
|
6450
6368
|
return;
|
|
6451
6369
|
try {
|
|
6452
|
-
startGroupTurn(groupId, text, resolveReplyTarget(threadId, replyToId), sendId, mode, id, { via });
|
|
6370
|
+
startGroupTurn(groupId, text, resolveReplyTarget(threadId, replyToId), sendId, mode, id, { via, threadId });
|
|
6453
6371
|
}
|
|
6454
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
|
+
}
|
|
6455
6376
|
store.appendMessage(threadId, {
|
|
6456
6377
|
role: "bot",
|
|
6457
6378
|
kind: "activity",
|
|
@@ -6464,6 +6385,7 @@ function drainQueuedChannelSends() {
|
|
|
6464
6385
|
// A message with no eligible responder creates no operation. Continue
|
|
6465
6386
|
// draining instead of leaving later user messages behind it forever.
|
|
6466
6387
|
queueMicrotask(drainQueuedChannelSends);
|
|
6388
|
+
return groupQueues.get(groupId);
|
|
6467
6389
|
});
|
|
6468
6390
|
}
|
|
6469
6391
|
function sameCalendarRoster(group, botIds) {
|
|
@@ -7371,6 +7293,14 @@ function configStatus() {
|
|
|
7371
7293
|
showToolCalls: showToolCallsEnabled(cfg),
|
|
7372
7294
|
browser: builtInBrowserEnabled(cfg),
|
|
7373
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
|
+
},
|
|
7374
7304
|
// Which browser this server can give bots: the desktop app's surface,
|
|
7375
7305
|
// the agent-browser engine, or nothing yet (with the reason).
|
|
7376
7306
|
browserEngine: browserEngineSummary(),
|
|
@@ -7489,6 +7419,7 @@ async function reloadProviders() {
|
|
|
7489
7419
|
});
|
|
7490
7420
|
store.setTaskActivity(botId, threadId, "idle");
|
|
7491
7421
|
}
|
|
7422
|
+
settleDirectFollowup(owner?.generation);
|
|
7492
7423
|
retryDelegationsWaitingOn(botId);
|
|
7493
7424
|
}
|
|
7494
7425
|
for (const [threadId, speaker] of rooms) {
|
|
@@ -10506,6 +10437,9 @@ const handleRequest = async (req, res) => {
|
|
|
10506
10437
|
}
|
|
10507
10438
|
const receipt = await sendSequencer.run(sendId ? `group:${group.id}:${threadId}:${sendId}` : undefined, sendFingerprint(text, replyTo?.id, channelMode), async () => {
|
|
10508
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
|
+
}
|
|
10509
10443
|
const accepted = acceptedSendMatch(store.messagesFor(threadId), sendId, text, replyTo?.id, channelMode);
|
|
10510
10444
|
if (accepted.kind === "conflict") {
|
|
10511
10445
|
throw Object.assign(new Error("sendId already belongs to another message"), { status: 409 });
|
|
@@ -11101,12 +11035,6 @@ const handleRequest = async (req, res) => {
|
|
|
11101
11035
|
error: "This approval-level change can only be made from the packaged desktop app",
|
|
11102
11036
|
});
|
|
11103
11037
|
}
|
|
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
11038
|
// "Auto on this Mac" hands a bot the user's real session, so the grant
|
|
11111
11039
|
// must prove a human saw the warning. The desktop dialog is the only
|
|
11112
11040
|
// caller that sends acknowledgeLocalAuto; without it a PATCH that would
|
|
@@ -11462,6 +11390,7 @@ const handleRequest = async (req, res) => {
|
|
|
11462
11390
|
// staged provider images into a message, so dispose them here.
|
|
11463
11391
|
for (const task of store.tasks(bot.id)) {
|
|
11464
11392
|
purgeGeneratedImagesForThread(task.threadId);
|
|
11393
|
+
settleDirectFollowup(directTurnGenerationByThread.get(task.threadId));
|
|
11465
11394
|
directTurnGenerationByThread.delete(task.threadId);
|
|
11466
11395
|
directTurnBots.delete(task.threadId);
|
|
11467
11396
|
}
|
|
@@ -12005,6 +11934,9 @@ const handleRequest = async (req, res) => {
|
|
|
12005
11934
|
const replyTo = resolveReplyTarget(threadId, body.replyToId);
|
|
12006
11935
|
const receipt = await sendSequencer.run(sendId ? `bot:${bot.id}:${threadId}:${sendId}` : undefined, sendFingerprint(text, replyTo?.id), async () => {
|
|
12007
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
|
+
}
|
|
12008
11940
|
const accepted = acceptedSendMatch(store.messagesFor(threadId), sendId, text, replyTo?.id);
|
|
12009
11941
|
if (accepted.kind === "conflict") {
|
|
12010
11942
|
throw Object.assign(new Error("sendId already belongs to another message"), { status: 409 });
|
|
@@ -12204,7 +12136,7 @@ const handleRequest = async (req, res) => {
|
|
|
12204
12136
|
resolvePeerComms(approvalBus, String(body.requestId), behavior)) {
|
|
12205
12137
|
return json(res, 200, { ok: true, outcome: behavior === "allow" ? "allowed-once" : "rejected" });
|
|
12206
12138
|
}
|
|
12207
|
-
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);
|
|
12208
12140
|
return json(res, 200, { ok: true, outcome });
|
|
12209
12141
|
}
|
|
12210
12142
|
// Answer by THREAD, so a request raised inside a room can be answered
|
|
@@ -12289,7 +12221,7 @@ const handleRequest = async (req, res) => {
|
|
|
12289
12221
|
if (!owner && !pending)
|
|
12290
12222
|
return json(res, 404, { error: "nothing is waiting on an answer in this conversation" });
|
|
12291
12223
|
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);
|
|
12224
|
+
const outcome = await answerRequest(threadId, requestOwner?.modelSelection.instanceId ?? "", requestId, behavior, body.message, owner ? { id: owner.id, name: owner.name } : undefined, body.always === true);
|
|
12293
12225
|
return json(res, 200, { ok: true, outcome });
|
|
12294
12226
|
}
|
|
12295
12227
|
m = path.match(/^\/api\/bots\/([\w-]+)\/interrupt$/);
|
|
@@ -12546,6 +12478,7 @@ const handleRequest = async (req, res) => {
|
|
|
12546
12478
|
const updated = store.deleteTask(m[1], m[2]);
|
|
12547
12479
|
if (!updated)
|
|
12548
12480
|
return json(res, 400, { error: "a bot keeps at least one task" });
|
|
12481
|
+
settleDirectFollowup(directTurnGenerationByThread.get(m[2]));
|
|
12549
12482
|
rejectDeletedThreadSkillStages(stagedSkillCleanups);
|
|
12550
12483
|
const fresh = botWithThread(updated);
|
|
12551
12484
|
broadcast({ kind: "bot", bot: fresh });
|
|
@@ -13641,19 +13574,10 @@ const handleRequest = async (req, res) => {
|
|
|
13641
13574
|
browserReferenceCleanupError = error;
|
|
13642
13575
|
}
|
|
13643
13576
|
}
|
|
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");
|
|
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);
|
|
13657
13581
|
// Config is already durable. A provider credential or runtime change
|
|
13658
13582
|
// invalidates every old child immediately, including when browser
|
|
13659
13583
|
// cleanup below has to await Electron before reloadProviders begins.
|
|
@@ -14149,8 +14073,45 @@ try {
|
|
|
14149
14073
|
catch (error) {
|
|
14150
14074
|
console.warn(`attachments: startup partial cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
14151
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();
|
|
14152
14110
|
server.listen(PORT, "127.0.0.1", () => {
|
|
14153
14111
|
console.log(`openmausbot server on http://127.0.0.1:${PORT}`);
|
|
14112
|
+
followupsReady = true;
|
|
14113
|
+
drainQueuedSends();
|
|
14114
|
+
drainQueuedChannelSends();
|
|
14154
14115
|
// Startup work uses the same turn dispatcher and local tool endpoint as
|
|
14155
14116
|
// ordinary chat. Start only once every registry is initialized and the
|
|
14156
14117
|
// endpoint is listening; earlier dispatch can hit uninitialized bindings.
|
|
@@ -14188,6 +14149,7 @@ if (TUNNEL_SOCKET) {
|
|
|
14188
14149
|
const gracefulShutdown = createGracefulShutdown({
|
|
14189
14150
|
cleanup: [
|
|
14190
14151
|
() => {
|
|
14152
|
+
followupsReady = false;
|
|
14191
14153
|
// Child MCP processes and the HTTP listener can remain alive while the
|
|
14192
14154
|
// asynchronous shutdown jobs drain. Invalidate their turn bearers before
|
|
14193
14155
|
// any cleanup function reaches an await.
|