@chibitek/chibi 0.1.154 → 0.1.157
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/{abnfDiagram-O67JEVCF-CbIXKQpc.js → abnfDiagram-O67JEVCF-BkYbog6D.js} +1 -1
- package/dist/assets/{arc-D1xLD8SN.js → arc-QwTuMsxp.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-xZe7iJ9y.js → architectureDiagram-NJMV4G6O-GdicO2PQ.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-Cq35uL03.js → blockDiagram-BEXU5L5S-Dkr29TFF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DjlN_-0r.js → c4Diagram-YGBWAQC7-Dz_3dstx.js} +1 -1
- package/dist/assets/channel-D2y0i2ps.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BQpywZWX.js → chunk-2Q5K7J3B-CGsfu-PI.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BajOpOBu.js → chunk-3FUC2YCW-CDqI37_6.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DuZpQtDE.js → chunk-5VM5RSS4-DnmSuyTb.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dn8xlUsI.js → chunk-GTNCS2PH-Dh7tXcJT.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Xrus5lAx.js → chunk-GWA4HPMP-D_SM1ZrY.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-Dhf8RCqM.js → chunk-JWPE2WC7-DmeMuTsI.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BQXmOQMl.js → chunk-NETBCI7D-R1UT9b5f.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BeraxyW-.js → chunk-WEXAMYUT-DpPfU1yO.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BsKuu9tP.js → chunk-XXDRQBXY-DlihfTkM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CcR0y3zf.js → classDiagram-v2-NBCMYWYE-BlTV6lSC.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Dd2y43ln.js → cose-bilkent-JH36ORCC-Br1kPFWw.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-DL4Zf255.js → cynefin-OW5HDTMX-BqPp3ZMd.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-CvfZKzI5.js → cynefinDiagram-VND7K2PF-DFfs_k8-.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-CZfm2BmL.js → dagre-6A5THRUB-Ch3xIJA7.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DEEoufNr.js → diagram-22UHCM2B-DIHUD87N.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-C1R6O8j1.js → diagram-3UASUU5V-CAgQQdeF.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-D9dPGToa.js → diagram-ATOU4E4O-9HVKodrI.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-BHy3uSmJ.js → diagram-CDSNMT55-DU2kDq_h.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CEo2WbRd.js → diagram-MLGK6HIB-D-qTAhHD.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-BrZrCrGC.js → diagram-MPIPVDR6-C31SnI9P.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-CtChmI9r.js → ebnfDiagram-ZINNZB2B-BHrdiBVd.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-C-qEle2N.js → elk-276RUBZZ-DWyljRo5.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BEdc5DT9.js → erDiagram-OPXOYQCR-DyY59XPp.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CT-fOp9_.js → flowDiagram-KWPJA3E3-BevMIeAy.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-LWgFxnGJ.js → ganttDiagram-FUAMR5RP-Ct18WHf4.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-xHSa3-lE.js → gitGraphDiagram-X574FWY7-yhixO1N3.js} +1 -1
- package/dist/assets/{index-Dy25lFJN.js → index-BEJ8EKQt.js} +1 -1
- package/dist/assets/index-DGWeJcck.css +1 -0
- package/dist/assets/{index-BpHS2C5J.js → index-ziyH_uer.js} +170 -168
- package/dist/assets/{infoDiagram-VRGFBTTK-BOyODyhP.js → infoDiagram-VRGFBTTK-CYa-r_S5.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-D3F47TMU.js → ishikawaDiagram-OU5B5YK6-CnxwM7gY.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-B8h_aacv.js → journeyDiagram-ZHPQQLJL-RKQfXBrp.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-nqNasrdQ.js → kanban-definition-PNTS6WVX-CSlRDIF6.js} +1 -1
- package/dist/assets/{linear-CDofN9-p.js → linear-Ch19KVyt.js} +1 -1
- package/dist/assets/{mermaid.core-k18eNV07.js → mermaid.core-Bek7I_t6.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-BYNmNDXA.js → mindmap-definition-NLK3R4M7-B_LiP8su.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-44c5V9Pa.js → pegDiagram-GJSIUBJH-DhzQeiZf.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-BDvznIYT.js → pieDiagram-5QR66LMP-D-BYNKHS.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BWrzS8SI.js → quadrantDiagram-O4NWA36T-DnCm0heP.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-H_3uxTrq.js → railroadDiagram-XR7U4H2S-COqgYnhn.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BiyKTF3R.js → requirementDiagram-PLB6GJNP-Dvt72ajf.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-43RmpN9N.js → sankeyDiagram-IPEJSGJF-CisC9nbp.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B0OUYqhV.js → sequenceDiagram-PO4LG4MO-BO8xqvxa.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-xiXiEFtk.js → sizeCapture-INFHLROL-DLk1jBzf.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-C4-l5TZJ.js → stateDiagram-v2-GCMORJYK-C_wutSE7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-B58p5Kvz.js → swimlanes-2SLR337P-hqo1UlEi.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D15JqTL9.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-DUgR2Qsq.js → timeline-definition-EJHVYXUP-C_DJldPg.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DcicVGBU.js → usecaseDiagram-POWQR4AR-CsidVXMA.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-9pThMhQ1.js → vennDiagram-UO4OBE2U-Bs-IIqn_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-CStNs1Wv.js → wardleyDiagram-VNRHLVJA-CXEUyxaj.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-JmQMXi-y.js → xychartDiagram-PMCCYNJV-B9qvmm4P.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +81 -81
- package/dist-server/drivers/agents-proxy.js +81 -81
- package/dist-server/index.js +2447 -1767
- package/dist-server/mcp-gate.js +7 -4
- package/dist-server/openmausbot.js +692 -665
- package/dist-server/pair-cli.js +692 -665
- package/dist-server/server/app-installers.js +107 -7
- package/dist-server/server/cli-help.js +74 -0
- package/dist-server/server/cli.js +32 -74
- package/dist-server/server/device-routing.js +60 -0
- package/dist-server/server/drivers/claude.js +13 -11
- package/dist-server/server/drivers/codex.js +18 -23
- package/dist-server/server/drivers/local-inject.js +3 -4
- package/dist-server/server/engine-tiers.js +5 -14
- package/dist-server/server/incidents.js +8 -4
- package/dist-server/server/index.js +98 -120
- package/dist-server/server/mcp-auth-signal.js +2 -1
- package/dist-server/server/mcp-gate-config.js +1 -0
- package/dist-server/server/mcp-gate.js +7 -4
- package/dist-server/server/mcp-health.js +14 -4
- package/dist-server/server/mcp-turn.js +26 -0
- package/dist-server/server/personal-goal-runtime.js +86 -0
- package/dist-server/server/personal-goal-turn.js +125 -0
- package/dist-server/server/personal-goals.js +195 -0
- package/dist-server/server/routes/local-models.js +3 -2
- package/dist-server/server/skill-fetch.js +14 -0
- package/dist-server/server/system-prompt.js +14 -6
- package/dist-server/shared/engine-choices.js +15 -0
- package/dist-server/shared/engine-tiers.js +74 -2
- package/dist-server/shared/local-models.js +1 -3
- package/dist-server/shared/personal-goal.js +43 -0
- package/dist-server/vps-container-mcp.js +81 -81
- package/package.json +1 -1
- package/dist/assets/channel-Bm-MISs3.js +0 -1
- package/dist/assets/index-C8YP7Zxj.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CPlL77A3.js +0 -8
|
@@ -78,6 +78,8 @@ function whatHappened(incident) {
|
|
|
78
78
|
return `${incident.bot.name}'s run ${where} continued on another model because its engine refused the one it was set to${detail}`;
|
|
79
79
|
case "engine-fallback":
|
|
80
80
|
return `${incident.bot.name}'s run ${where} continued on another engine after its selected engine failed${detail}`;
|
|
81
|
+
case "mcp-unavailable":
|
|
82
|
+
return `${incident.bot.name}'s run ${where} continued without one of its tool servers${detail}`;
|
|
81
83
|
default:
|
|
82
84
|
return `${incident.bot.name}'s run ${where} failed${detail}`;
|
|
83
85
|
}
|
|
@@ -99,13 +101,15 @@ export function incidentText(incident, count) {
|
|
|
99
101
|
lines.push(`${incident.bot.name} last said: "${fold(incident.lastReply, 300)}"`);
|
|
100
102
|
if (count.count > 1)
|
|
101
103
|
lines.push(`This is the ${ordinal(count.count)} incident on that thread within the hour.`);
|
|
102
|
-
if (incident.kind === "model-fallback" || incident.kind === "engine-fallback") {
|
|
103
|
-
// Nothing broke: the turn ran. The failed selection or
|
|
104
|
-
// needs attention, but the completed work must never be retried.
|
|
104
|
+
if (incident.kind === "model-fallback" || incident.kind === "engine-fallback" || incident.kind === "mcp-unavailable") {
|
|
105
|
+
// Nothing broke: the turn ran. The failed selection, engine or tool
|
|
106
|
+
// server still needs attention, but the completed work must never be retried.
|
|
105
107
|
lines.push("The run was not interrupted and needs no retry; do not call retry_thread.");
|
|
106
108
|
lines.push(incident.kind === "model-fallback"
|
|
107
109
|
? "Tell the person in one or two plain sentences that this engine no longer offers that model and that the bot's or the conversation's model setting should be changed, then stop."
|
|
108
|
-
:
|
|
110
|
+
: incident.kind === "mcp-unavailable"
|
|
111
|
+
? "Tell the person in one or two plain sentences which tool server is not working, that bots carry on without it until it answers again, and that its command and settings in Settings may need a look, then stop."
|
|
112
|
+
: "Use the incident detail to correct the failed engine if action is needed, then report what happened in one or two plain sentences and stop.");
|
|
109
113
|
return lines.join("\n");
|
|
110
114
|
}
|
|
111
115
|
lines.push(count.mayRetry
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { hasUnboundGoalGuard, goalGuardForEvent, addGoalGuard, removeGoalGuard, runPersonalGoalTurn, updatePersonalGoalReceipt } from "./personal-goal-turn.js";
|
|
2
|
+
import { PersonalGoalRuntime, receiveGoalCommand } from "./personal-goal-runtime.js";
|
|
3
|
+
import { personalGoalInstructions } from "./personal-goals.js";
|
|
4
|
+
import { parseGoalCommand } from "../shared/personal-goal.js";
|
|
1
5
|
// OpenMausBot server — the harness host. Clients hold no transports
|
|
2
6
|
// (upstream rule): the React app dispatches typed commands over HTTP and
|
|
3
7
|
// folds one SSE event stream; every provider process runs here.
|
|
@@ -92,7 +96,7 @@ import { probeMcpServer } from "./mcp-probe.js";
|
|
|
92
96
|
import { McpToolHintStore, readOnlyToolAllowed } from "./mcp-tool-hints.js";
|
|
93
97
|
import { McpAuthStateStore } from "./mcp-auth-state.js";
|
|
94
98
|
import { McpHealthStore } from "./mcp-health.js";
|
|
95
|
-
import {
|
|
99
|
+
import { mcpTurnPolicy } from "./mcp-turn.js";
|
|
96
100
|
import { mcpAuthSignalDir } from "./mcp-gate-config.js";
|
|
97
101
|
import { bridgeMcpRemote } from "./mcp-remote-bridge.js";
|
|
98
102
|
import { mcpToolRefFromLabel } from "../shared/mcp-tool-ref.js";
|
|
@@ -143,7 +147,7 @@ import { MEMORY_INDEX, MemoryStoreError, memoryCapacity, memoryOverview, openFol
|
|
|
143
147
|
import { beginMemoryTurn, endMemoryTurn, flushAllMemoryJournals, flushMemoryJournal, journalMemoryDelete, journalMemoryWrite, readMemoryJournal, revertMemoryChange, } from "./memory-journal.js";
|
|
144
148
|
import { readSectionContext, readSections, sectionContextKey, sectionContextLabel, sectionContextSystemPrompt, writeSectionContext, SECTION_CONTEXT_MAX_BYTES, } from "./section-context.js";
|
|
145
149
|
import { applyStagedSkillWrite, applySkillWriteWithReceipt, getStagedSkillWrite, installSkill, parseSkillMd, scanSkillText, listSkills, listStagedSkillWrites, isSkillName, readSkillFile, rejectStagedSkillWrite, removeSkill, setSkillEnabled, skillsSystemPrompt, stageSkillWrite, } from "./skills.js";
|
|
146
|
-
import {
|
|
150
|
+
import { resolveSkillImport, skillImportSchema } from "./skill-fetch.js";
|
|
147
151
|
import { pickBotName } from "./names.js";
|
|
148
152
|
import { botSkillTemplateSchema, newBotDefaultsSchema, resolveBotCreationDefaults } from "./new-bot-defaults.js";
|
|
149
153
|
import { expandLearnTurnText, learnSource } from "./skill-learn.js";
|
|
@@ -235,6 +239,7 @@ import { activityCsv, activityEntries, appendAdminAction, botAuditSnapshot, botC
|
|
|
235
239
|
import { frameForMember, memberBody, memberBot, noteSeen, notFoundFor, audienceEquals, memberGroup, narrowestAudience, parseVisibility, pathSubject, roomFeeds, routineVisible, sameAudience, SEES_EVERYTHING, SEES_ALL_BUT_PRIVATE, VisibleSet, } from "./bot-visibility.js";
|
|
236
240
|
import { describeBrand, loadBrand } from "./brand.js";
|
|
237
241
|
import { deliverSseFrame } from "./sse-fanout.js";
|
|
242
|
+
import { DeviceRouteRegistry, removeStream } from "./device-routing.js";
|
|
238
243
|
import { PHONE_SECRET_PROTOCOL_VERSION, PhoneSecretBridge, PhoneSecretError, PhoneSecretSubmissionRegistry, assertPhoneSecretRequestMatches, phoneSecretOperationId, } from "./phone-secret.js";
|
|
239
244
|
// Keep these two last: a route module may import any server module, and
|
|
240
245
|
// loading the table after everything above leaves module start-up order as is.
|
|
@@ -248,7 +253,7 @@ import { createModelLevelsRoutes } from "./routes/model-levels.js";
|
|
|
248
253
|
import { createMcpMarketplaceRoutes } from "./routes/mcp-marketplace.js";
|
|
249
254
|
import { createLocalModelRoutes, OLLAMA_LOCAL_URL, ollamaInstalledHere } from "./routes/local-models.js";
|
|
250
255
|
import { createSetupRoutes } from "./routes/setup.js";
|
|
251
|
-
import { installOllamaApp, openPodmanInstaller, runTool, startPodmanMachine } from "./app-installers.js";
|
|
256
|
+
import { installOllamaApp, openPodmanInstaller, startOllamaApp, runTool, startPodmanMachine } from "./app-installers.js";
|
|
252
257
|
import { CliLoginRunner } from "./cli-login.js";
|
|
253
258
|
import { JobFailure, SetupJobs } from "./setup-jobs.js";
|
|
254
259
|
import { localHost, probeLocalInjects } from "./drivers/local-inject.js";
|
|
@@ -1820,6 +1825,7 @@ function requestedTaskBot(botId, rawThreadId) {
|
|
|
1820
1825
|
return task;
|
|
1821
1826
|
}
|
|
1822
1827
|
async function interruptDirectThread(botId, threadId) {
|
|
1828
|
+
personalGoals.steer(threadId);
|
|
1823
1829
|
const requestOwner = directRequestOwners.get(threadId);
|
|
1824
1830
|
if (requestOwner) {
|
|
1825
1831
|
requestOwner.stopped = true;
|
|
@@ -3350,7 +3356,7 @@ function localModeTurn(resolution, turn) {
|
|
|
3350
3356
|
return { ...resolution, local: true, choice: { ...choice, selection: placement.selection, candidate: placement.candidate } };
|
|
3351
3357
|
}
|
|
3352
3358
|
const payPerUsePausedLine = (until) => `Pay-per-use is paused until ${restTime(until)}, when a subscription is back, so this turn stopped.`;
|
|
3353
|
-
/** A model this machine serves:
|
|
3359
|
+
/** A model this machine serves: a rung when a ladder names it, else local rules only. */
|
|
3354
3360
|
const isLocalModel = (candidate, model) => localModel(candidate, model) || modelLevels.classOf(candidate.driverKind, model)?.local === true;
|
|
3355
3361
|
// ── silent automatic failover (docs/chibitek/engine-tiers.md, step 2) ────
|
|
3356
3362
|
// Resting engines are shared by every bot and survive a restart.
|
|
@@ -3589,10 +3595,7 @@ const eventBase = (event) => ({
|
|
|
3589
3595
|
...(event.providerInstanceId ? { providerInstanceId: event.providerInstanceId } : {}), ...(event.turnId ? { turnId: event.turnId } : {}),
|
|
3590
3596
|
});
|
|
3591
3597
|
const restTime = (at) => new Date(at).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
3592
|
-
/**
|
|
3593
|
-
* out. Rest the engine, then run the same turn on the next engine at the
|
|
3594
|
-
* same level, silently; or, when nothing is left, stop it with one line
|
|
3595
|
-
* naming the resting engines. Returns what subscribers see. */
|
|
3598
|
+
/** Rest a refused engine and continue on an eligible engine, or show an actionable line. */
|
|
3596
3599
|
function engineFailover(turn, completion) {
|
|
3597
3600
|
const failure = turn.failure;
|
|
3598
3601
|
const record = failoverTurns.get(turn.threadId);
|
|
@@ -3615,17 +3618,14 @@ function engineFailover(turn, completion) {
|
|
|
3615
3618
|
const restFailed = () => engineRests.rest(turn.instanceId, failedCandidate ? engineAccount(failedCandidate) : undefined, failure.kind, { resetAt: failure.resetAt, resetText: failure.resetText, windowMs: failure.windowMs, message: redactSecretsInText(failure.message),
|
|
3616
3619
|
...(failure.permanent ? { permanent: true } : {}), ...(scope ? { scope } : {}), ...(ran?.model ? { model: ran.model } : {}),
|
|
3617
3620
|
...(failure.kind === "auth_failed" && failedCandidate ? { reportedSignedIn: failedCandidate.snapshot.state === "available" && failedCandidate.snapshot.authenticated === true } : {}) });
|
|
3618
|
-
//
|
|
3619
|
-
// next engine: the engine rests (the circuit breaker) only once the turn
|
|
3620
|
-
// really moves, and with nowhere to go the turn ends with the engine's own
|
|
3621
|
-
// report, exactly as before, rather than a "resting" line about it.
|
|
3621
|
+
// Unknown failures rest only when the turn moves; otherwise preserve their report.
|
|
3622
3622
|
const rest = failure.kind === "provider_error" ? undefined : restFailed();
|
|
3623
3623
|
const label = (instanceId) => candidates.find((candidate) => candidate.instanceId === instanceId)?.displayName ?? instanceId;
|
|
3624
3624
|
// The one plain line, never the provider's own words: which account needs
|
|
3625
3625
|
// the person and where to fix it, or that every engine is resting.
|
|
3626
3626
|
const stop = (line, category = "failover_exhausted") => [
|
|
3627
3627
|
{ ...eventBase(completion), type: "runtime.error", message: line, terminal: true, category, ...(failure.setup ? { setup: true } : {}) },
|
|
3628
|
-
completion,
|
|
3628
|
+
failure.kind === "auth_failed" ? { ...completion, stopReason: "auth_required" } : completion,
|
|
3629
3629
|
];
|
|
3630
3630
|
const restingNamed = (instanceIds) => [...new Set(instanceIds)].flatMap((instanceId) => {
|
|
3631
3631
|
const candidate = candidates.find((entry) => entry.instanceId === instanceId);
|
|
@@ -3648,13 +3648,11 @@ function engineFailover(turn, completion) {
|
|
|
3648
3648
|
return stop(pick.line, pick.category);
|
|
3649
3649
|
const { choice } = pick;
|
|
3650
3650
|
const tried = new Set(record.tried);
|
|
3651
|
-
//
|
|
3652
|
-
// chip, no notification, no incident, and nothing settled that the same
|
|
3653
|
-
// turn still owns.
|
|
3651
|
+
// Retire the failed attempt without settling the request that the next engine owns.
|
|
3654
3652
|
const moved = ran ?? record.selection;
|
|
3655
3653
|
const failedOwner = turnResourceOwners.get(turn.threadId);
|
|
3656
3654
|
const tokens = completion.usage ?? turnUsage.get(turn.threadId);
|
|
3657
|
-
const partialText = turn.partial.trim();
|
|
3655
|
+
const partialText = groupGoalCoordinatorTurns.get(turn.threadId)?.size ? parseGroupGoalDecision(turn.partial).visibleText : turn.partial.trim();
|
|
3658
3656
|
const requestOwner = directRequestOwners.get(turn.threadId);
|
|
3659
3657
|
if (partialText) {
|
|
3660
3658
|
// The answer it was in the middle of stays: the person saw it stream.
|
|
@@ -3666,6 +3664,12 @@ function engineFailover(turn, completion) {
|
|
|
3666
3664
|
const tools = earlier.filter((message) => message.kind === "activity" && message.tool?.name).map((message) => message.tool.name);
|
|
3667
3665
|
const note = continuationNote(said.map((message) => message.text ?? ""), tools);
|
|
3668
3666
|
const attempt = record.attempt + 1;
|
|
3667
|
+
for (const guard of groupGoalCoordinatorTurns.get(turn.threadId) ?? []) {
|
|
3668
|
+
if (!guard.discard && (guard.turnId === completion.turnId || guard.turnId === turn.turnId)) {
|
|
3669
|
+
guard.turnId = undefined;
|
|
3670
|
+
guard.assistantItems = [];
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3669
3673
|
const rerun = startTurn(turn.botId, note ? `${record.text}\n\n${note}` : record.text, {
|
|
3670
3674
|
...record.opts,
|
|
3671
3675
|
threadId: turn.threadId,
|
|
@@ -3703,6 +3707,11 @@ function engineFailover(turn, completion) {
|
|
|
3703
3707
|
});
|
|
3704
3708
|
}
|
|
3705
3709
|
retireFailedAttempt(turn.botId, turn.threadId, turn.generation, failedOwner, completion);
|
|
3710
|
+
if (failure.kind === "auth_failed")
|
|
3711
|
+
reportIncident({
|
|
3712
|
+
kind: "engine-fallback", bot: profile, threadId: turn.threadId, notifyChief: true,
|
|
3713
|
+
detail: `${engineLabel(candidates, moved)} needs signing in again in Settings > Engines; the turn continued on ${engineLabel(candidates, choice.selection)}`,
|
|
3714
|
+
});
|
|
3706
3715
|
console.error(`[omb-failover] thread=${turn.threadId} ${failure.kind}: ${moved.instanceId} -> ${choice.selection.instanceId} (attempt ${attempt})`);
|
|
3707
3716
|
// No turnId: the failed provider turn is retired, and its id would hide
|
|
3708
3717
|
// this record from every window.
|
|
@@ -5013,67 +5022,21 @@ const publicBotQueuedMessages = () => queuedSteerSnapshot((botId, threadId) => B
|
|
|
5013
5022
|
const groupTurnOperations = new Map();
|
|
5014
5023
|
const groupGoalCoordinatorTurns = new Map();
|
|
5015
5024
|
const GROUP_GOAL_COORDINATOR_GUARD_MS = 5 * 60_000;
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
.some((turn) => turn.discard && !turn.turnId);
|
|
5032
|
-
}
|
|
5033
|
-
/** Match private coordinator output to one provider turn, never merely to a
|
|
5034
|
-
* reusable room thread. Most adapters emit turn.started before sendTurn
|
|
5035
|
-
* resolves, so the first stable event may bind an otherwise pending guard. */
|
|
5036
|
-
function groupGoalCoordinatorTurnForEvent(event) {
|
|
5037
|
-
const turns = groupGoalCoordinatorTurns.get(event.threadId);
|
|
5038
|
-
if (!turns?.size)
|
|
5039
|
-
return undefined;
|
|
5040
|
-
const candidates = [...turns];
|
|
5041
|
-
if (event.turnId) {
|
|
5042
|
-
const exact = candidates.find((turn) => turn.turnId === event.turnId);
|
|
5043
|
-
if (exact)
|
|
5044
|
-
return exact;
|
|
5045
|
-
// Until an interrupted handshake returns its own id, no new id can be
|
|
5046
|
-
// attributed safely. The stall fallback keeps this thread unavailable in
|
|
5047
|
-
// that narrow window; private text is suppressed below until sendTurn's
|
|
5048
|
-
// result binds the old guard or its bounded expiry releases ownership.
|
|
5049
|
-
const unboundDiscarded = candidates.filter((turn) => turn.discard && !turn.turnId);
|
|
5050
|
-
if (unboundDiscarded.length > 0) {
|
|
5051
|
-
// The ownership fallback below keeps a lone abandoned handshake's
|
|
5052
|
-
// thread closed, so its first eventual id can safely bind here. More
|
|
5053
|
-
// than one unbound candidate is genuinely ambiguous and stays gated.
|
|
5054
|
-
if (candidates.length === 1) {
|
|
5055
|
-
unboundDiscarded[0].turnId = event.turnId;
|
|
5056
|
-
// This event is the first stable identity for an already-abandoned
|
|
5057
|
-
// provider turn. Tombstone it immediately so this event and every
|
|
5058
|
-
// later completion/request cannot settle a replacement on the same
|
|
5059
|
-
// room thread.
|
|
5060
|
-
retireProviderTurn(event.turnId);
|
|
5061
|
-
return unboundDiscarded[0];
|
|
5062
|
-
}
|
|
5063
|
-
return undefined;
|
|
5064
|
-
}
|
|
5065
|
-
const pending = candidates.findLast((turn) => !turn.turnId && !turn.discard);
|
|
5066
|
-
if (pending && !pending.turnId) {
|
|
5067
|
-
pending.turnId = event.turnId;
|
|
5068
|
-
return pending;
|
|
5069
|
-
}
|
|
5070
|
-
return undefined;
|
|
5071
|
-
}
|
|
5072
|
-
// Turn-scoped events normally carry an id. If an adapter omits it, fail
|
|
5073
|
-
// closed for private text; with multiple overlapping guards there is no
|
|
5074
|
-
// safe way to attribute a completion, so leave cleanup to the bounded timer.
|
|
5075
|
-
return candidates.length === 1 ? candidates[0] : undefined;
|
|
5076
|
-
}
|
|
5025
|
+
const personalGoals = new PersonalGoalRuntime(join(DATA_DIR, "personal-goals.json"), {
|
|
5026
|
+
busy: (botId, threadId) => threadBusy(botId, threadId) || Boolean(activeGroupTurnForBot(botId)),
|
|
5027
|
+
interrupt: async (botId, threadId, goalId) => {
|
|
5028
|
+
const owns = [...(groupGoalCoordinatorTurns.get(threadId) ?? [])].some(guard => guard.personalGoalId === goalId && !guard.discard && (!guard.turnId || guard.turnId === liveTurnByThread.get(threadId)));
|
|
5029
|
+
if (owns)
|
|
5030
|
+
await interruptDirectThread(botId, threadId);
|
|
5031
|
+
},
|
|
5032
|
+
changed: (goal) => updatePersonalGoalReceipt(goal, store),
|
|
5033
|
+
run: (goal) => runPersonalGoalTurn(goal, { store, addGuard: addGroupGoalCoordinatorTurn, removeGuard: removeGroupGoalCoordinatorTurn,
|
|
5034
|
+
subscribe: (listener) => bus.subscribe(listener), ignore: shouldIgnoreProviderEvent, busy: threadBusy, start: startTurn }),
|
|
5035
|
+
});
|
|
5036
|
+
const addGroupGoalCoordinatorTurn = (threadId, turn) => addGoalGuard(groupGoalCoordinatorTurns, threadId, turn);
|
|
5037
|
+
const removeGroupGoalCoordinatorTurn = (threadId, turn) => removeGoalGuard(groupGoalCoordinatorTurns, threadId, turn);
|
|
5038
|
+
const hasUnboundDiscardedGroupGoalTurn = (threadId) => hasUnboundGoalGuard(groupGoalCoordinatorTurns, threadId);
|
|
5039
|
+
const groupGoalCoordinatorTurnForEvent = (event) => goalGuardForEvent(groupGoalCoordinatorTurns, retireProviderTurn, event);
|
|
5077
5040
|
function groupIsWorking(group) {
|
|
5078
5041
|
return Boolean(group.busyBotId) || Boolean(groupTurnOperations.get(group.id)?.size);
|
|
5079
5042
|
}
|
|
@@ -6024,6 +5987,9 @@ function messageWindow(threadId, messageId, limit) {
|
|
|
6024
5987
|
return { messages: all.slice(start, stop).map(slimMessage), hasMore: start > 0 };
|
|
6025
5988
|
}
|
|
6026
5989
|
const sseClients = new Set();
|
|
5990
|
+
// Approval device routing: docs/specs/remote-approval/spec.md.
|
|
5991
|
+
const deviceRoutes = new DeviceRouteRegistry();
|
|
5992
|
+
const removeSseClient = (client) => removeStream(sseClients, deviceRoutes, client);
|
|
6027
5993
|
/** The frame at which a bot's audience last changed (see the SSE route). */
|
|
6028
5994
|
let audienceChangedAt = 0;
|
|
6029
5995
|
/** A bot's audience changed: end every member stream. Each reconnects at
|
|
@@ -6035,27 +6001,15 @@ function audienceChanged() {
|
|
|
6035
6001
|
for (const client of sseClients) {
|
|
6036
6002
|
if (client.viewer.kind !== "member")
|
|
6037
6003
|
continue;
|
|
6038
|
-
|
|
6039
|
-
try {
|
|
6040
|
-
client.res.end();
|
|
6041
|
-
}
|
|
6042
|
-
catch {
|
|
6043
|
-
/* already gone */
|
|
6044
|
-
}
|
|
6004
|
+
removeSseClient(client);
|
|
6045
6005
|
}
|
|
6046
6006
|
}
|
|
6047
6007
|
function closeSessionStreams(sessionId) {
|
|
6048
6008
|
browserLive.closeForOwner(sessionId);
|
|
6049
|
-
for (const client of sseClients) {
|
|
6009
|
+
for (const client of Array.from(sseClients)) {
|
|
6050
6010
|
if (client.sessionId !== sessionId)
|
|
6051
6011
|
continue;
|
|
6052
|
-
|
|
6053
|
-
try {
|
|
6054
|
-
client.res.end();
|
|
6055
|
-
}
|
|
6056
|
-
catch {
|
|
6057
|
-
/* already gone */
|
|
6058
|
-
}
|
|
6012
|
+
removeSseClient(client);
|
|
6059
6013
|
}
|
|
6060
6014
|
}
|
|
6061
6015
|
sessions.onSessionRevoked((sessionId) => {
|
|
@@ -6253,7 +6207,7 @@ function broadcast(payload, options = {}) {
|
|
|
6253
6207
|
// Screen frames are replaceable and durable events are not: see
|
|
6254
6208
|
// ./sse-fanout.ts for the backpressure/bound decision this makes.
|
|
6255
6209
|
if (deliverSseFrame(client, kind, out) === "disconnected") {
|
|
6256
|
-
|
|
6210
|
+
removeSseClient(client);
|
|
6257
6211
|
}
|
|
6258
6212
|
}
|
|
6259
6213
|
}
|
|
@@ -8258,6 +8212,8 @@ bus.subscribe((event) => {
|
|
|
8258
8212
|
lastReply.delete(event.threadId);
|
|
8259
8213
|
const turnError = lastTurnError.get(event.threadId);
|
|
8260
8214
|
lastTurnError.delete(event.threadId);
|
|
8215
|
+
// A tool server that broke under this turn is reported with the turn (mcp-turn.ts), not at whatever turn next starts.
|
|
8216
|
+
syncMcpAuthState();
|
|
8261
8217
|
// A run that broke — not one the person stopped, and not a routine's,
|
|
8262
8218
|
// which reports through its own failure path — is the Chief's to see.
|
|
8263
8219
|
// A lazy computer-claim rejection already reported its failure and
|
|
@@ -8269,7 +8225,7 @@ bus.subscribe((event) => {
|
|
|
8269
8225
|
if (!event.ok && event.stopReason !== "interrupted" && !lazyClaimAlreadyReported && !routines?.runForThread(event.threadId)) {
|
|
8270
8226
|
const broken = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
8271
8227
|
if (broken)
|
|
8272
|
-
reportIncident({ kind: "failed", bot: broken, threadId: event.threadId, detail: failedRunDetail(event.stopReason, turnError) });
|
|
8228
|
+
reportIncident({ kind: "failed", bot: broken, threadId: event.threadId, detail: failedRunDetail(event.stopReason, turnError), notifyChief: event.stopReason === "auth_required" });
|
|
8273
8229
|
}
|
|
8274
8230
|
const lastReported = turnUsage.get(event.threadId);
|
|
8275
8231
|
turnUsage.delete(event.threadId);
|
|
@@ -8625,17 +8581,17 @@ function incidentContext(threadId) {
|
|
|
8625
8581
|
}
|
|
8626
8582
|
function reportIncident(input) {
|
|
8627
8583
|
const { bot, threadId } = input;
|
|
8628
|
-
// A fallback is a run that went on, not a broken one.
|
|
8629
|
-
const quiet = input.kind === "model-fallback" || input.kind === "engine-fallback";
|
|
8584
|
+
// A fallback, or a turn that ran on without a broken tool server, is a run that went on, not a broken one.
|
|
8585
|
+
const quiet = input.kind === "model-fallback" || input.kind === "engine-fallback" || input.kind === "mcp-unavailable";
|
|
8630
8586
|
// The Primary Bot's pre-check reads broken runs from here (a failed
|
|
8631
8587
|
// routine has its own run record, so it is not noted twice).
|
|
8632
|
-
if (input.kind !== "routine-failed" && input.kind !== "model-fallback" && input.kind !== "engine-fallback") {
|
|
8588
|
+
if (input.kind !== "routine-failed" && input.kind !== "model-fallback" && input.kind !== "engine-fallback" && input.kind !== "mcp-unavailable") {
|
|
8633
8589
|
primaryBotStore.noteFailure({ botId: bot.id, threadId, kind: input.kind, detail: redactSecretsInText(input.detail).slice(0, 300), at: Date.now() });
|
|
8634
8590
|
}
|
|
8635
8591
|
const task = store.taskByThread(bot.id, threadId);
|
|
8636
8592
|
// A thread another bot opened and is watching is that bot's to handle:
|
|
8637
8593
|
// the delegator is woken with the failure already (wakeDelegationSource).
|
|
8638
|
-
if (task?.openedBy?.delegationId || delegationWatch.has(threadId) || roomHandoffs.activeDirect(threadId))
|
|
8594
|
+
if (!input.notifyChief && (task?.openedBy?.delegationId || delegationWatch.has(threadId) || roomHandoffs.activeDirect(threadId)))
|
|
8639
8595
|
return;
|
|
8640
8596
|
const group = store.groupByThread(threadId);
|
|
8641
8597
|
const incident = {
|
|
@@ -10758,6 +10714,7 @@ async function startTurn(botId, text, opts) {
|
|
|
10758
10714
|
? await nexusMemory.readBack({ bot: liveBot ?? bot, threadId, query: providerText })
|
|
10759
10715
|
: "";
|
|
10760
10716
|
const prompt = buildSystemPrompt(persona, liveBot?.soul ?? bot.soul ?? "", roomPromptSections(roomTurn, [
|
|
10717
|
+
{ id: "personal-goal", label: "Active goal", text: personalGoals.goals.get(threadId)?.status === "working" ? personalGoalInstructions(personalGoals.goals.get(threadId)) : "" },
|
|
10761
10718
|
{ id: "user-profile", label: "About the user", text: userProfileSystemPrompt(cfg.profile) },
|
|
10762
10719
|
// the ICM folder is on this machine; a cloud run cannot reach it
|
|
10763
10720
|
// first after the soul: the block names agent tools, so it only goes
|
|
@@ -10775,7 +10732,7 @@ async function startTurn(botId, text, opts) {
|
|
|
10775
10732
|
{ id: "team-computer", label: "Team computer", text: teamComputerPrompt(teamComputer) },
|
|
10776
10733
|
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: mountedComputer, browser: Boolean(integrations.browser) }, { pinned: plan.pinned, note: plan.note, canSelect: computerSelectionTurns.has(threadId) }) },
|
|
10777
10734
|
{ id: "accounts", label: "Google and Microsoft accounts", text: integrations.accounts ? accountsPrompt(botAccountEmails(bot)) : "" },
|
|
10778
|
-
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {})) },
|
|
10735
|
+
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {}), instance.adapter.capabilities.customMcp === true ? mcpTurn.unavailable(engineMcpServers(bot)) : []) },
|
|
10779
10736
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
10780
10737
|
{ id: "coordination", label: "Team", text: coordinationPrompt ? ` ${coordinationPrompt}` : "" },
|
|
10781
10738
|
{ id: "assignment", label: "Teammate task", text: coordinationNode ? `\n${coordinationSystemInstructions()}` : "" },
|
|
@@ -12623,7 +12580,7 @@ failover) {
|
|
|
12623
12580
|
{ id: "private-bot", label: "Private Bot", text: isPersonalBot(bot) ? privateBotPrompt(false) : "" },
|
|
12624
12581
|
{ id: "files", label: "File locations", text: workspace ? workspaceLocationsPrompt(bot.id, cwd, readyBot.cwd) : "" },
|
|
12625
12582
|
{ id: "repos", label: "Linked repos", text: repoPlan ? repoPromptSection(repoPlan, repoPlace(undefined, roomTeamComputer || roomComputerKind === "box" ? "box" : roomVmMounted ? "vm" : roomComputerKind), cwd) : "" },
|
|
12626
|
-
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {})) },
|
|
12583
|
+
{ id: "mcp", label: "MCP servers", text: customMcpPrompt(Object.keys(integrations.custom ?? {}), instance.adapter.capabilities.customMcp === true ? mcpTurn.unavailable(engineMcpServers(bot)) : []) },
|
|
12627
12584
|
{ id: "computer", label: "Computer", text: computerPrompt(roomTeamComputer || roomComputerKind === "box" ? instance.driverKind === "boxAgent" ? "box-agent" : instance.adapter.capabilities.cloudComputerMcp ? "box-chat" : "box" : roomVmMounted ? localVmMode(cfg) === "per-bot" ? "vm-private" : "vm-shared" : roomComputerKind) },
|
|
12628
12585
|
{ id: "team-computer", label: "Team computer", text: teamComputerPrompt(roomTeamComputer) },
|
|
12629
12586
|
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: roomTeamComputer ? "cloud" : roomVmMounted ? "vm" : surfaceOfComputerKind(roomComputerKind), browser: Boolean(integrations.browser) }, { note: roomPlan.note }) },
|
|
@@ -14781,23 +14738,19 @@ function warmMcpToolHints(custom) {
|
|
|
14781
14738
|
/** Each configured server's sign-in state, and whether it is otherwise broken. */
|
|
14782
14739
|
const mcpAuthState = new McpAuthStateStore(join(DATA_DIR, "mcp-auth-state.json"));
|
|
14783
14740
|
const mcpHealth = new McpHealthStore();
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
for (const state of [mcpAuthState, mcpHealth])
|
|
14789
|
-
state.ingest(signals, customMcpServers(cfg));
|
|
14790
|
-
}
|
|
14741
|
+
const mcpTurn = mcpTurnPolicy({ auth: mcpAuthState, health: mcpHealth, signalDir: mcpAuthSignalDir, specs: () => customMcpServers(cfg),
|
|
14742
|
+
reportUnavailable: (threadId, detail) => { const bot = store.botByThread(threadId); if (bot)
|
|
14743
|
+
reportIncident({ kind: "mcp-unavailable", bot, threadId, detail }); } });
|
|
14744
|
+
function syncMcpAuthState() { mcpTurn.sync(); }
|
|
14791
14745
|
/** How a start runs an mcp-remote bridge: background starts never open a
|
|
14792
14746
|
* browser; only a person's click in Settings may (server/mcp-remote-bridge.ts). */
|
|
14793
14747
|
function bridgedMcpServer(spec, allowBrowser) {
|
|
14794
14748
|
return bridgeMcpRemote(spec, { bridge: resolveMcpRemoteBridge(), execPath: process.execPath, allowBrowser });
|
|
14795
14749
|
}
|
|
14796
|
-
/** The servers one turn starts
|
|
14797
|
-
* is backing off or stopped, or that lately would not start or crashed. */
|
|
14750
|
+
/** The servers one turn starts (mcp-turn.ts), after folding in what the gates reported. */
|
|
14798
14751
|
function turnMcpServers(bot) {
|
|
14799
14752
|
syncMcpAuthState();
|
|
14800
|
-
return
|
|
14753
|
+
return mcpTurn.mountable(engineMcpServers(bot));
|
|
14801
14754
|
}
|
|
14802
14755
|
function engineMcpServers(bot) {
|
|
14803
14756
|
// Rule 4 (server/personal-bot.ts): a bot with no list of its own gets every
|
|
@@ -16131,7 +16084,7 @@ ROUTES.push(createSetupRoutes({
|
|
|
16131
16084
|
}
|
|
16132
16085
|
},
|
|
16133
16086
|
startOllama: async (context) => {
|
|
16134
|
-
|
|
16087
|
+
startOllamaApp();
|
|
16135
16088
|
for (let attempt = 0; attempt < 30 && !context.signal.aborted; attempt++) {
|
|
16136
16089
|
if ((await setupOllamaState()) === "running")
|
|
16137
16090
|
return;
|
|
@@ -19064,6 +19017,7 @@ const handleRequest = async (req, res) => {
|
|
|
19064
19017
|
admin: auth.scopes.includes("admin"),
|
|
19065
19018
|
screens: url.searchParams.get("screens") !== "off",
|
|
19066
19019
|
backpressured: false,
|
|
19020
|
+
authKind: auth.kind,
|
|
19067
19021
|
viewer,
|
|
19068
19022
|
// What this person's hydration (GET /api/bots, narrowed the same way) showed them.
|
|
19069
19023
|
seen: viewer.kind === "all" && viewer.privateOwner ? { bots: new Set(), groups: new Set() } : {
|
|
@@ -19107,6 +19061,7 @@ const handleRequest = async (req, res) => {
|
|
|
19107
19061
|
// A client that offered no cursor gets false too, which is exactly
|
|
19108
19062
|
// what a cold start should do.
|
|
19109
19063
|
resumed,
|
|
19064
|
+
approvalTarget: deviceRoutes.route(),
|
|
19110
19065
|
})}\n\n`);
|
|
19111
19066
|
if (resumed) {
|
|
19112
19067
|
for (const buffered of replayBuffer) {
|
|
@@ -19122,6 +19077,7 @@ const handleRequest = async (req, res) => {
|
|
|
19122
19077
|
}
|
|
19123
19078
|
}
|
|
19124
19079
|
sseClients.add(client);
|
|
19080
|
+
deviceRoutes.streamOpened(client.authKind);
|
|
19125
19081
|
// Keep this long-lived response out of socket idle-timeout handling
|
|
19126
19082
|
// without weakening timeouts for every other API request.
|
|
19127
19083
|
req.socket.setTimeout(0);
|
|
@@ -19141,7 +19097,7 @@ const handleRequest = async (req, res) => {
|
|
|
19141
19097
|
}, SSE_HEARTBEAT_MS);
|
|
19142
19098
|
req.on("close", () => {
|
|
19143
19099
|
clearInterval(keepalive);
|
|
19144
|
-
|
|
19100
|
+
removeSseClient(client);
|
|
19145
19101
|
});
|
|
19146
19102
|
return;
|
|
19147
19103
|
}
|
|
@@ -20586,11 +20542,11 @@ const handleRequest = async (req, res) => {
|
|
|
20586
20542
|
});
|
|
20587
20543
|
}
|
|
20588
20544
|
if (method === "POST" && path === "/api/bot-defaults/skills/preview") {
|
|
20589
|
-
|
|
20545
|
+
// Drafts accept the same uploaded bytes as existing bots, without installing them.
|
|
20546
|
+
const checked = skillImportSchema.safeParse(await readBody(req));
|
|
20590
20547
|
if (!checked.success)
|
|
20591
20548
|
return json(res, 400, { error: checked.error.message });
|
|
20592
|
-
const
|
|
20593
|
-
const fetched = await fetchSkillFromSource(input.source);
|
|
20549
|
+
const fetched = await resolveSkillImport(checked.data);
|
|
20594
20550
|
if ("error" in fetched)
|
|
20595
20551
|
return json(res, 422, { error: fetched.error });
|
|
20596
20552
|
const skills = [];
|
|
@@ -21703,10 +21659,10 @@ const handleRequest = async (req, res) => {
|
|
|
21703
21659
|
if (m && method === "POST") {
|
|
21704
21660
|
if (!store.bot(m[1]))
|
|
21705
21661
|
return json(res, 404, { error: "no such bot" });
|
|
21706
|
-
const parsed =
|
|
21662
|
+
const parsed = skillImportSchema.safeParse(await readBody(req));
|
|
21707
21663
|
if (!parsed.success)
|
|
21708
|
-
return json(res, 400, { error: "
|
|
21709
|
-
const fetched = await
|
|
21664
|
+
return json(res, 400, { error: "Choose a Markdown skill file or enter a GitHub source (maximum 256 KiB per file)." });
|
|
21665
|
+
const fetched = await resolveSkillImport(parsed.data);
|
|
21710
21666
|
if ("error" in fetched)
|
|
21711
21667
|
return json(res, 422, { error: fetched.error });
|
|
21712
21668
|
const results = fetched.skills.map((skill) => installSkill(m[1], skill.source, skill.files));
|
|
@@ -22235,10 +22191,20 @@ const handleRequest = async (req, res) => {
|
|
|
22235
22191
|
userAgent: typeof req.headers["user-agent"] === "string" ? req.headers["user-agent"] : undefined,
|
|
22236
22192
|
relayed: Boolean(onBehalfOf),
|
|
22237
22193
|
}));
|
|
22194
|
+
let goalCommand;
|
|
22195
|
+
try {
|
|
22196
|
+
goalCommand = parseGoalCommand(text);
|
|
22197
|
+
}
|
|
22198
|
+
catch (error) {
|
|
22199
|
+
return json(res, 400, { error: error instanceof Error ? error.message : "Invalid goal command" });
|
|
22200
|
+
}
|
|
22201
|
+
if (guarded && goalCommand)
|
|
22202
|
+
return json(res, 400, { error: "Goal commands require the conversation command interface." });
|
|
22238
22203
|
// The send is acknowledged before the turn starts, so a workspace at its
|
|
22239
22204
|
// spend limit is refused here, where the person can see it.
|
|
22240
22205
|
try {
|
|
22241
|
-
|
|
22206
|
+
if (!goalCommand || goalCommand.action === "start" || goalCommand.action === "resume")
|
|
22207
|
+
assertWithinBudget(cfg, DATA_DIR);
|
|
22242
22208
|
}
|
|
22243
22209
|
catch (error) {
|
|
22244
22210
|
return json(res, 409, { error: error instanceof Error ? error.message : String(error), code: "spend_cap" });
|
|
@@ -22307,6 +22273,12 @@ const handleRequest = async (req, res) => {
|
|
|
22307
22273
|
const message = await startTurn(bot.id, text, { threadId, replyTo, sendId, sender: messageSender(auth), trigger });
|
|
22308
22274
|
return { ok: true, threadId, message };
|
|
22309
22275
|
}
|
|
22276
|
+
if (goalCommand) {
|
|
22277
|
+
if (auth.kind === "assistant")
|
|
22278
|
+
throw Object.assign(new Error("Only the person controls goals."), { status: 403 });
|
|
22279
|
+
return receiveGoalCommand(personalGoals, store, bot.id, threadId, goalCommand, text, sendId, messageSender(auth), trigger);
|
|
22280
|
+
}
|
|
22281
|
+
personalGoals.steer(threadId);
|
|
22310
22282
|
// Claude can accept the message inside its live turn. If the write
|
|
22311
22283
|
// loses a race with turn settlement, or the engine cannot steer, the
|
|
22312
22284
|
// existing server-side queue records it atomically for the next turn.
|
|
@@ -23735,6 +23707,8 @@ const handleRequest = async (req, res) => {
|
|
|
23735
23707
|
if (method === "GET" && authStatus) {
|
|
23736
23708
|
res.setHeader("cache-control", "no-store");
|
|
23737
23709
|
const state = await providerAuthSessions.status(authStatus[1], auth.kind === "session" ? auth.session.id : "loopback", url.searchParams.get("flowId") ?? "");
|
|
23710
|
+
if (state.phase === "succeeded")
|
|
23711
|
+
engineRests.clear(authStatus[1], "signed-in");
|
|
23738
23712
|
return json(res, 200, { auth: state });
|
|
23739
23713
|
}
|
|
23740
23714
|
const instanceAction = /^\/api\/instances\/([\w.-]+)\/(refresh-models|install|auth\/start|auth\/complete|auth\/cancel|auth\/sign-out)$/.exec(path);
|
|
@@ -23776,6 +23750,8 @@ const handleRequest = async (req, res) => {
|
|
|
23776
23750
|
providerAuthSessions.revokeOwner(owner);
|
|
23777
23751
|
return json(res, 401, { error: "Your session ended. Start a new sign-in." });
|
|
23778
23752
|
}
|
|
23753
|
+
if (started.phase === "succeeded")
|
|
23754
|
+
engineRests.clear(instanceId, "signed-in");
|
|
23779
23755
|
return json(res, 200, { auth: started });
|
|
23780
23756
|
}
|
|
23781
23757
|
if (action === "auth/sign-out") {
|
|
@@ -23793,6 +23769,8 @@ const handleRequest = async (req, res) => {
|
|
|
23793
23769
|
if (!flowId || !callbackUrl)
|
|
23794
23770
|
return json(res, 400, { error: "flowId and a code or callbackUrl are required" });
|
|
23795
23771
|
await providerAuthSessions.complete(instanceId, owner, flowId, callbackUrl);
|
|
23772
|
+
engineRests.clear(instanceId, "signed-in");
|
|
23773
|
+
await describeEngines();
|
|
23796
23774
|
return json(res, 200, { ok: true });
|
|
23797
23775
|
}
|
|
23798
23776
|
const body = await readBody(req, 4096);
|
|
@@ -45,7 +45,8 @@ export function drainMcpAuthSignals(dir) {
|
|
|
45
45
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
46
46
|
if (typeof parsed.name === "string" && MCP_SIGNAL_KINDS.includes(parsed.kind)
|
|
47
47
|
&& typeof parsed.at === "number" && Number.isFinite(parsed.at)) {
|
|
48
|
-
signals.push({ name: parsed.name, kind: parsed.kind, at: parsed.at
|
|
48
|
+
signals.push({ name: parsed.name, kind: parsed.kind, at: parsed.at,
|
|
49
|
+
...(typeof parsed.threadId === "string" && parsed.threadId ? { threadId: parsed.threadId } : {}) });
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
catch {
|
|
@@ -40,9 +40,12 @@ const SPILL_MAX_AGE_MS = 24 * 60 * 60_000;
|
|
|
40
40
|
const SPILL_HINT = process.env.OMB_GATE_SPILL_HINT === "1";
|
|
41
41
|
/** The gate's own settings never reach the upstream server's environment. */
|
|
42
42
|
const GATE_ENV_KEYS = ["OMB_GATE_NAME", "OMB_GATE_SPILL_DIR", "OMB_GATE_BUDGET", "OMB_GATE_UPSTREAM", "OMB_GATE_SPILL_HINT", "OMB_GATE_AUTH_DIR",
|
|
43
|
-
"OMB_GATE_START_TIMEOUT_MS"];
|
|
43
|
+
"OMB_GATE_START_TIMEOUT_MS", "OMB_GATE_THREAD"];
|
|
44
44
|
/** Where this gate reports whether its server's sign-in worked. */
|
|
45
45
|
const AUTH_DIR = process.env.OMB_GATE_AUTH_DIR || "";
|
|
46
|
+
/** The turn this gate serves, so a failure can be reported for its bot. */
|
|
47
|
+
const THREAD = process.env.OMB_GATE_THREAD || "";
|
|
48
|
+
const signalHarness = (kind) => writeMcpAuthSignal(AUTH_DIR, { name: NAME, kind, at: Date.now(), ...(THREAD ? { threadId: THREAD } : {}) });
|
|
46
49
|
function fail(message) {
|
|
47
50
|
process.stderr.write(`mcp-gate(${NAME}): ${message}\n`);
|
|
48
51
|
process.exit(1);
|
|
@@ -196,7 +199,7 @@ function goDown(reason, why, signal = true) {
|
|
|
196
199
|
clearTimeout(startTimer);
|
|
197
200
|
process.stderr.write(`mcp-gate(${NAME}): ${why}; answering as an empty server so the turn carries on without it\n`);
|
|
198
201
|
if (signal)
|
|
199
|
-
|
|
202
|
+
signalHarness(reason);
|
|
200
203
|
for (const message of inFlight.values())
|
|
201
204
|
answer(strandedResponse(NAME, message));
|
|
202
205
|
inFlight.clear();
|
|
@@ -290,7 +293,7 @@ createInterface({ input: child.stdout }).on("line", (line) => {
|
|
|
290
293
|
reportedOk = true;
|
|
291
294
|
if (startTimer)
|
|
292
295
|
clearTimeout(startTimer);
|
|
293
|
-
|
|
296
|
+
signalHarness("ok");
|
|
294
297
|
}
|
|
295
298
|
const tool = id === undefined ? undefined : pending.get(id);
|
|
296
299
|
if (id !== undefined)
|
|
@@ -317,7 +320,7 @@ child.on("exit", (code, signal) => {
|
|
|
317
320
|
return;
|
|
318
321
|
if (!reportedOk && exitMeansSignInRequired(code, stderrTail)) {
|
|
319
322
|
process.stderr.write(`mcp-gate(${NAME}): sign-in required; not opening a browser from a background start\n`);
|
|
320
|
-
|
|
323
|
+
signalHarness("sign-in-required");
|
|
321
324
|
if (!clientGone)
|
|
322
325
|
return goDown("start-failed", "needs a sign-in", false);
|
|
323
326
|
}
|
|
@@ -29,12 +29,13 @@ export class McpHealthStore {
|
|
|
29
29
|
noteFailed(name, spec, kind, at = this.now()) {
|
|
30
30
|
const entry = this.entry(name, spec);
|
|
31
31
|
if (entry && at < entry.retryAt)
|
|
32
|
-
return;
|
|
32
|
+
return false;
|
|
33
33
|
const failures = (entry?.failures ?? 0) + 1;
|
|
34
34
|
const wait = MCP_FAILURE_BACKOFF_MS[Math.min(failures, MCP_FAILURE_BACKOFF_MS.length) - 1];
|
|
35
35
|
const retryAt = at + wait;
|
|
36
36
|
this.servers.set(name, { fingerprint: mcpServerFingerprint(spec), kind, failures, lastFailureAt: at, retryAt });
|
|
37
37
|
this.log(`[mcp] "${name}" ${kind === "crashed" ? "stopped during a turn" : "would not start"}; turns run without it until ${formatTime(retryAt)}`);
|
|
38
|
+
return true;
|
|
38
39
|
}
|
|
39
40
|
/** The server answered: whatever was wrong is over. An answer from a start
|
|
40
41
|
* that began before a later failure does not clear that failure. */
|
|
@@ -54,17 +55,26 @@ export class McpHealthStore {
|
|
|
54
55
|
const when = this.now() >= entry.retryAt ? "Atomic tries it again on the next turn." : `Atomic tries it again at ${formatTime(entry.retryAt)}.`;
|
|
55
56
|
return `This server ${what}, so bots are working without it for now. ${when} Check its command and settings if this keeps happening.`;
|
|
56
57
|
}
|
|
58
|
+
/** Why a server is left out of turns right now, or undefined when it mounts. */
|
|
59
|
+
unavailableKind(name, spec) {
|
|
60
|
+
const entry = this.entry(name, spec);
|
|
61
|
+
return entry && this.now() < entry.retryAt ? entry.kind : undefined;
|
|
62
|
+
}
|
|
57
63
|
/** Apply what gates and the chat runtime reported, in order. `specs` are
|
|
58
|
-
* the servers as configured now; a signal for a removed server is dropped.
|
|
64
|
+
* the servers as configured now; a signal for a removed server is dropped.
|
|
65
|
+
* Returns the signals that recorded a new failure (a repeat inside the
|
|
66
|
+
* backoff window is not one), so the harness can raise one incident each. */
|
|
59
67
|
ingest(signals, specs) {
|
|
68
|
+
const fresh = [];
|
|
60
69
|
for (const signal of signals) {
|
|
61
70
|
const spec = specs[signal.name];
|
|
62
71
|
if (!spec)
|
|
63
72
|
continue;
|
|
64
73
|
if (signal.kind === "ok")
|
|
65
74
|
this.noteOk(signal.name, signal.at);
|
|
66
|
-
else if (signal.kind === "start-failed" || signal.kind === "crashed")
|
|
67
|
-
|
|
75
|
+
else if ((signal.kind === "start-failed" || signal.kind === "crashed") && this.noteFailed(signal.name, spec, signal.kind, signal.at))
|
|
76
|
+
fresh.push(signal);
|
|
68
77
|
}
|
|
78
|
+
return fresh;
|
|
69
79
|
}
|
|
70
80
|
}
|